feat: 新增ai_chat_core,将core 和 widget 分离(未完成)
迁移了 models enums, utils, http 封装,还有一些extensions;service 只迁移了 sse service
This commit is contained in:
90
packages/ai_chat_core/lib/ai_chat_core.dart
Normal file
90
packages/ai_chat_core/lib/ai_chat_core.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
建议目录结构:
|
||||
```
|
||||
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)
|
||||
*/
|
||||
|
||||
// ignore: unnecessary_library_name
|
||||
library ai_chat_core;
|
||||
|
||||
// enums
|
||||
export 'src/enums/message_status.dart';
|
||||
export 'src/enums/message_service_state.dart';
|
||||
export 'src/enums/vehicle_command_type.dart';
|
||||
|
||||
// models
|
||||
export 'src/models/chat_message.dart';
|
||||
export 'src/models/vehicle_status_info.dart';
|
||||
export 'src/models/vehicle_cmd.dart';
|
||||
export 'src/models/vehicle_cmd_response.dart';
|
||||
|
||||
// extensions
|
||||
export 'src/extensions/string_extension.dart';
|
||||
export 'src/extensions/color_extension.dart';
|
||||
|
||||
// utils
|
||||
export 'src/utils/markdown_cleaner.dart';
|
||||
|
||||
// services
|
||||
export 'src/services/chat_sse_service.dart';
|
||||
export 'src/services/tts_service.dart';
|
||||
|
||||
// http client
|
||||
export 'src/network/api_config.dart';
|
||||
export 'src/network/http_client.dart';
|
||||
9
packages/ai_chat_core/lib/src/apis/assistant_api.dart
Normal file
9
packages/ai_chat_core/lib/src/apis/assistant_api.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
// assistant_api.dart
|
||||
class AssistantAPI {
|
||||
static const String baseUrl = 'https://api.example.com';
|
||||
static const String chatEndpoint = '/v1/chat';
|
||||
static const String ttsEndpoint = '/v1/tts';
|
||||
static const String imageEndpoint = '/v1/image';
|
||||
static const String videoEndpoint = '/v1/video';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/// 消息服务状态
|
||||
enum MessageServiceState {
|
||||
// 空闲
|
||||
idle,
|
||||
// 录音中
|
||||
recording,
|
||||
// 识别中
|
||||
recognizing,
|
||||
// 回复中
|
||||
replying,
|
||||
}
|
||||
17
packages/ai_chat_core/lib/src/enums/message_status.dart
Normal file
17
packages/ai_chat_core/lib/src/enums/message_status.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
// Message状态枚举
|
||||
enum MessageStatus {
|
||||
normal('普通消息', 'Normal'),
|
||||
listening('聆听中', 'Listening'),
|
||||
recognizing('识别中', 'Recognizing'),
|
||||
thinking('思考中', 'Thinking'),
|
||||
completed('完成回答', 'Completed'),
|
||||
executing('执行中', 'Executing'),
|
||||
success('执行成功', 'Success'),
|
||||
failure('执行失败', 'Failure'),
|
||||
aborted('已中止', 'Aborted');
|
||||
|
||||
const MessageStatus(this.chinese, this.english);
|
||||
|
||||
final String chinese;
|
||||
final String english;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 车控指令类型
|
||||
enum VehicleCommandType {
|
||||
// 未知
|
||||
unknown('未知', 'unknown'),
|
||||
// 上锁车门
|
||||
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');
|
||||
|
||||
const VehicleCommandType(this.chinese, this.english);
|
||||
|
||||
final String chinese;
|
||||
final String english;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
static const Color commonColor = Color(0xFF6F72F1);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
extension StringExtension on String {
|
||||
bool get isChinese {
|
||||
final chineseRegex = RegExp(r'^[\u4e00-\u9fa5]+$');
|
||||
return chineseRegex.hasMatch(this);
|
||||
}
|
||||
|
||||
bool get isEmail {
|
||||
final emailRegex = RegExp(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
|
||||
return emailRegex.hasMatch(this);
|
||||
}
|
||||
|
||||
bool get isPhoneNumber {
|
||||
final phoneRegex = RegExp(r'^\+?[0-9]{7,15}$');
|
||||
return phoneRegex.hasMatch(this);
|
||||
}
|
||||
|
||||
bool get isValidUrl {
|
||||
final urlRegex = RegExp(
|
||||
r'^(https?:\/\/)?' // 协议
|
||||
r'(([a-zA-Z0-9_-]+\.)+[a-zA-Z]{2,6})' // 域名
|
||||
r'(\/[a-zA-Z0-9._%+-]*)*\/?' // 路径
|
||||
r'(\?[a-zA-Z0-9=&%+-]*)?' // 查询参数
|
||||
r'(#[a-zA-Z0-9_-]*)?$'); // 锚点
|
||||
return urlRegex.hasMatch(this);
|
||||
}
|
||||
|
||||
String capitalize() {
|
||||
if (isEmpty) return this;
|
||||
return this[0].toUpperCase() + substring(1);
|
||||
}
|
||||
|
||||
String toSnakeCase() {
|
||||
return replaceAllMapped(
|
||||
RegExp(r'[A-Z]'), (Match match) => '_${match.group(0)!.toLowerCase()}')
|
||||
.replaceFirst('_', '');
|
||||
}
|
||||
|
||||
String toKebabCase() {
|
||||
return replaceAllMapped(
|
||||
RegExp(r'[A-Z]'), (Match match) => '-${match.group(0)!.toLowerCase()}')
|
||||
.replaceFirst('-', '');
|
||||
}
|
||||
|
||||
String toTitleCase() {
|
||||
return split(' ')
|
||||
.map((word) =>
|
||||
word.isEmpty ? word : word[0].toUpperCase() + word.substring(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
String reverse() {
|
||||
return split('').reversed.join();
|
||||
}
|
||||
}
|
||||
17
packages/ai_chat_core/lib/src/models/chat_message.dart
Normal file
17
packages/ai_chat_core/lib/src/models/chat_message.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import '../enums/message_status.dart';
|
||||
|
||||
class ChatMessage {
|
||||
final String id;
|
||||
final String text;
|
||||
final bool isUser;
|
||||
final DateTime timestamp;
|
||||
MessageStatus status;
|
||||
|
||||
ChatMessage({
|
||||
required this.id,
|
||||
required this.text,
|
||||
required this.isUser,
|
||||
required this.timestamp,
|
||||
this.status = MessageStatus.normal,
|
||||
});
|
||||
}
|
||||
40
packages/ai_chat_core/lib/src/models/vehicle_cmd.dart
Normal file
40
packages/ai_chat_core/lib/src/models/vehicle_cmd.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import '../enums/vehicle_command_type.dart';
|
||||
|
||||
/// 车辆控制命令类
|
||||
class VehicleCommand {
|
||||
final VehicleCommandType type;
|
||||
final Map<String, dynamic>? params;
|
||||
final String error;
|
||||
late String commandId;
|
||||
|
||||
VehicleCommand({required this.type, this.params, this.error = ''}) {
|
||||
commandId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
}
|
||||
|
||||
// 从字符串创建命令(用于从API响应解析)
|
||||
factory VehicleCommand.fromString(
|
||||
String commandStr, Map<String, dynamic>? params, String error) {
|
||||
// 将字符串转换为枚举值
|
||||
VehicleCommandType type;
|
||||
try {
|
||||
type = VehicleCommandType.values.firstWhere(
|
||||
(e) => e.name == commandStr,
|
||||
orElse: () => VehicleCommandType.unknown,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error parsing command string: $e');
|
||||
// 默认为搜车指令,或者你可以选择抛出异常
|
||||
type = VehicleCommandType.unknown;
|
||||
}
|
||||
|
||||
return VehicleCommand(type: type, params: params, error: error);
|
||||
}
|
||||
|
||||
// 将命令转换为字符串(用于API请求)
|
||||
String get commandString => type.name;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VehicleCommand(command: ${type.name}, params: $params)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'vehicle_cmd.dart';
|
||||
|
||||
class VehicleCommandResponse {
|
||||
final String? tips;
|
||||
final List<VehicleCommand> commands;
|
||||
|
||||
VehicleCommandResponse({
|
||||
this.tips,
|
||||
required this.commands,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
class VehicleStatusInfo {
|
||||
/// 对应车架号
|
||||
String vin = "";
|
||||
|
||||
/// 剩余里程
|
||||
double remainingMileage = 0;
|
||||
|
||||
/// 空调开关状态 开启true,false关闭
|
||||
bool acState = false;
|
||||
|
||||
/// 空调温度 未拿到、不支持都返回0;原始值
|
||||
double acTemp = 0;
|
||||
|
||||
/// AC开关 true打开,false关闭
|
||||
bool acSwitch = false;
|
||||
|
||||
/// 左前锁状态 true解锁,false闭锁
|
||||
bool leftFrontLockState = false;
|
||||
|
||||
/// 右前锁状态 true解锁,false闭锁
|
||||
bool rightFrontLockState = false;
|
||||
|
||||
/// 左后锁状态 true解锁,false闭锁
|
||||
bool leftRearLockState = false;
|
||||
|
||||
/// 右后锁状态 true解锁,false闭锁
|
||||
bool rightRearLockState = false;
|
||||
|
||||
/// 左前门状态 true打开,false关闭
|
||||
bool leftFrontDoorState = false;
|
||||
|
||||
/// 右前门状态 true打开,false关闭
|
||||
bool rightFrontDoorState = false;
|
||||
|
||||
/// 左后门状态 true打开,false关闭
|
||||
bool leftRearDoorState = false;
|
||||
|
||||
/// 右后门状态 true打开,false关闭
|
||||
bool rightRearDoorState = false;
|
||||
|
||||
/// 左前窗状态 true打开,false关闭
|
||||
bool leftFrontWindowState = false;
|
||||
|
||||
/// 右前窗状态 true打开,false关闭
|
||||
bool rightFrontWindowState = false;
|
||||
|
||||
/// 左后窗状态 true打开,false关闭
|
||||
bool leftRearWindowState = false;
|
||||
|
||||
/// 右后窗状态 true打开,false关闭
|
||||
bool rightRearWindowState = false;
|
||||
|
||||
/// 后备箱状态 true打开,false关闭
|
||||
bool trunkState = false;
|
||||
|
||||
/// 电量百分比
|
||||
int soc = 0;
|
||||
|
||||
/// 车内温度
|
||||
double temperatureInside = 0;
|
||||
|
||||
/// 方向盘加热状态 false:未加热 true:加热中
|
||||
bool wheelHeat = false;
|
||||
|
||||
/// 主座椅加热状态 false:未加热 true:加热中
|
||||
bool mainSeatHeat = false;
|
||||
|
||||
/// 副座椅加热档位 false:未加热 true:加热中
|
||||
bool minorSeatHeat = false;
|
||||
|
||||
/// 是否行驶中,仅用于车控前置条件判断
|
||||
bool isDriving = false;
|
||||
|
||||
/// 会员是否过期 true过期,false不过期
|
||||
bool vipExpired = false;
|
||||
|
||||
/// 会员有效期,时间戳,单位毫秒
|
||||
int vipTime = 0;
|
||||
|
||||
/// 距离过期还剩多少天
|
||||
int vipRemainCount = 0;
|
||||
|
||||
/// 车况更新时间 ms
|
||||
int statusUpdateTime = 0;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IGKVehicleStatusInfo{vin:$vin, LockState:($leftFrontLockState,$rightFrontLockState,$leftRearLockState,$rightRearLockState), DoorState:($leftFrontDoorState,$rightFrontDoorState,$leftRearDoorState,$rightRearDoorState), WindowState: ($leftFrontWindowState,$rightFrontWindowState,$leftRearWindowState,$rightRearWindowState), trunkState: $trunkState, soc: $soc, remainingMileage: $remainingMileage, acState: $acState, acTemp: $acTemp, acSwitch:$acSwitch, isDriving:$isDriving, vipExpired: $vipExpired, vipTime: $vipTime, vipRemainCount: $vipRemainCount}';
|
||||
}
|
||||
}
|
||||
22
packages/ai_chat_core/lib/src/network/api_config.dart
Normal file
22
packages/ai_chat_core/lib/src/network/api_config.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
// API配置文件
|
||||
class ApiConfig {
|
||||
// API基础URL
|
||||
static const String baseURL = 'http://143.64.185.20:18606';
|
||||
/// 请求超时时间
|
||||
static const Duration timeout = Duration(seconds: 30);
|
||||
/// 默认请求头
|
||||
static const Map<String, String> defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
/// 重试次数
|
||||
static const int maxRetries = 2;
|
||||
|
||||
/// 每次重试间隔(毫秒)
|
||||
static const int retryDelayMs = 1000;
|
||||
|
||||
// 私有构造函数,防止实例化
|
||||
ApiConfig._();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'api_config.dart';
|
||||
import 'interceptor.dart';
|
||||
|
||||
class HeaderInterceptor extends Interceptor {
|
||||
@override
|
||||
Future<http.StreamedResponse> onRequest(InterceptorChain chain) async {
|
||||
chain.request.headers.addAll(ApiConfig.defaultHeaders);
|
||||
return await chain.next(chain.request);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> onResponse(http.StreamedResponse response) async {
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onError(Object error) async {}
|
||||
}
|
||||
151
packages/ai_chat_core/lib/src/network/http_client.dart
Normal file
151
packages/ai_chat_core/lib/src/network/http_client.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'api_config.dart';
|
||||
import 'header_interceptor.dart';
|
||||
import 'http_exception.dart';
|
||||
import 'interceptor.dart';
|
||||
import 'log_interceptor.dart';
|
||||
|
||||
class HttpClient extends http.BaseClient {
|
||||
final http.Client _inner;
|
||||
final List<Interceptor> _interceptors;
|
||||
|
||||
static List<Interceptor> defaultInterceptors = [
|
||||
HeaderInterceptor(),
|
||||
LogInterceptor(),
|
||||
];
|
||||
|
||||
HttpClient({
|
||||
http.Client? inner,
|
||||
List<Interceptor>? interceptors,
|
||||
}) : _inner = inner ?? http.Client(),
|
||||
_interceptors = interceptors ?? defaultInterceptors;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
int retries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
// 1请求拦截器链
|
||||
var response = await _executeRequestChain(request);
|
||||
|
||||
// 响应拦截器链
|
||||
response = await _executeResponseChain(response);
|
||||
|
||||
// 响应校验
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return response;
|
||||
} else {
|
||||
// 服务器返回错误码,包装成 HttpException 并抛出
|
||||
// 为了获取 body,我们需要读取流
|
||||
final bodyBytes = await response.stream.toBytes();
|
||||
final bodyString = utf8.decode(bodyBytes, allowMalformed: true);
|
||||
final data = bodyString.isNotEmpty ? json.decode(bodyString) : null;
|
||||
throw HttpException(
|
||||
code: response.statusCode,
|
||||
message: 'Request failed with status: ${response.statusCode}',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// 错误拦截器链
|
||||
await _executeErrorChain(e);
|
||||
|
||||
// 如果是可重试的错误 (例如网络问题),并且未达到最大次数
|
||||
if (e is SocketException && retries < ApiConfig.maxRetries) {
|
||||
retries++;
|
||||
print('⚠️ Network error, retrying... ($retries/${ApiConfig.maxRetries})');
|
||||
await Future.delayed(const Duration(milliseconds: ApiConfig.retryDelayMs));
|
||||
continue; // 继续下一次循环
|
||||
}
|
||||
|
||||
// 如果是 HttpException (服务器错误) 或其他不可重试的错误,或已达最大重试次数,则统一处理后抛出
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归执行请求拦截器链
|
||||
Future<http.StreamedResponse> _executeRequestChain(http.BaseRequest request, [int index = 0]) {
|
||||
if (index >= _interceptors.length) {
|
||||
// 拦截器链执行完毕,发出真正的网络请求
|
||||
return _inner.send(request);
|
||||
}
|
||||
// 获取当前拦截器
|
||||
final interceptor = _interceptors[index];
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
Next nextStep = (req) => _executeRequestChain(req, index + 1);
|
||||
|
||||
// 创建 InterceptorChain 实例,传入 request 和“下一步”的逻辑
|
||||
final chain = InterceptorChain(request, nextStep);
|
||||
|
||||
// 调用当前拦截器的 onRequest 方法
|
||||
return interceptor.onRequest(chain);
|
||||
}
|
||||
|
||||
/// 顺序执行响应拦截器链
|
||||
Future<http.StreamedResponse> _executeResponseChain(http.StreamedResponse response) async {
|
||||
var res = response;
|
||||
for (final interceptor in _interceptors) {
|
||||
res = await interceptor.onResponse(res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// 逆序执行错误拦截器链
|
||||
Future<void> _executeErrorChain(Object error) async {
|
||||
for (final interceptor in _interceptors.reversed) {
|
||||
await interceptor.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// 它负责将各种原始错误标准化为 HttpException
|
||||
HttpException _handleError(Object e) {
|
||||
if (e is HttpException) {
|
||||
return e; // 如果已经是 HttpException,直接返回
|
||||
} else if (e is SocketException) {
|
||||
return HttpException(message: '网络连接错误,请检查网络设置');
|
||||
} else if (e is TimeoutException) {
|
||||
return HttpException(message: '请求超时,请稍后重试');
|
||||
} else {
|
||||
return HttpException(message: '发生未知错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 串行请求
|
||||
Future<List<dynamic>> runSequential(List<Future<dynamic> Function()> tasks) async {
|
||||
final results = <dynamic>[];
|
||||
for (final task in tasks) {
|
||||
try {
|
||||
final res = await task();
|
||||
results.add(res);
|
||||
} catch (e) {
|
||||
results.add(e);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// 并行请求
|
||||
Future<List<dynamic>> runParallel(List<Future<dynamic>> tasks) async {
|
||||
final results = <dynamic>[];
|
||||
final completer = Completer<List<dynamic>>();
|
||||
int completed = 0;
|
||||
|
||||
for (final task in tasks) {
|
||||
task.then((result) {
|
||||
results.add(result);
|
||||
}).catchError((error) {
|
||||
results.add(error);
|
||||
}).whenComplete(() {
|
||||
completed++;
|
||||
if (completed == tasks.length) {
|
||||
completer.complete(results);
|
||||
}
|
||||
});
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
17
packages/ai_chat_core/lib/src/network/http_exception.dart
Normal file
17
packages/ai_chat_core/lib/src/network/http_exception.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
class HttpException implements Exception {
|
||||
|
||||
final int? code;
|
||||
final String message;
|
||||
final dynamic data;
|
||||
|
||||
HttpException({this.code, required this.message, this.data});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (code != null) {
|
||||
return 'HttpException: $message (code: $code)';
|
||||
}
|
||||
return 'HttpException: $message';
|
||||
}
|
||||
|
||||
}
|
||||
32
packages/ai_chat_core/lib/src/network/interceptor.dart
Normal file
32
packages/ai_chat_core/lib/src/network/interceptor.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
typedef Next = Future<http.StreamedResponse> Function(http.BaseRequest request);
|
||||
|
||||
/// 拦截器处理器,用于将请求/响应/错误传递给链中的下一个环节
|
||||
class InterceptorChain {
|
||||
final http.BaseRequest request;
|
||||
final Next _next;
|
||||
// 构造函数
|
||||
InterceptorChain(this.request, this._next);
|
||||
|
||||
/// 调用链中的下一个环节
|
||||
Future<http.StreamedResponse> next(http.BaseRequest req) {
|
||||
// 调用传入的 _next 函数
|
||||
return _next(req);
|
||||
}
|
||||
}
|
||||
|
||||
/// 拦截器抽象基类
|
||||
abstract class Interceptor {
|
||||
/// 请求被发送前调用
|
||||
/// 你可以在这里修改请求,例如添加 headers,然后必须调用 `chain.next(request)`
|
||||
Future<http.StreamedResponse> onRequest(InterceptorChain chain);
|
||||
|
||||
/// 收到响应后调用
|
||||
/// 你可以在这里读取和转换响应,然后返回一个新的响应
|
||||
Future<http.StreamedResponse> onResponse(http.StreamedResponse response);
|
||||
|
||||
/// 发生错误时调用
|
||||
/// 你可以在这里处理错误,例如重试,或者转换错误类型
|
||||
Future<void> onError(Object error);
|
||||
}
|
||||
37
packages/ai_chat_core/lib/src/network/log_interceptor.dart
Normal file
37
packages/ai_chat_core/lib/src/network/log_interceptor.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'interceptor.dart';
|
||||
|
||||
class LogInterceptor extends Interceptor {
|
||||
@override
|
||||
Future<http.StreamedResponse> onRequest(InterceptorChain chain) async {
|
||||
final request = chain.request;
|
||||
print('➡️ [${request.method}] ${request.url}');
|
||||
print('Headers: ${request.headers}');
|
||||
// 注意:对于流式请求,我们无法在这里直接打印 body
|
||||
return await chain.next(request);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> onResponse(http.StreamedResponse response) async {
|
||||
// 为了打印 body,我们需要读取流,然后重新创建一个
|
||||
final bodyBytes = await response.stream.toBytes();
|
||||
final bodyString = utf8.decode(bodyBytes, allowMalformed: true);
|
||||
print('⬅️ [${response.statusCode}] ${response.request?.url}');
|
||||
print('Response Body: $bodyString');
|
||||
|
||||
// 重新创建流并返回新的响应
|
||||
return http.StreamedResponse(
|
||||
Stream.value(bodyBytes),
|
||||
response.statusCode,
|
||||
headers: response.headers,
|
||||
request: response.request,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onError(Object error) async {
|
||||
print(' http Error: $error');
|
||||
}
|
||||
}
|
||||
44
packages/ai_chat_core/lib/src/services/channel_provider.dart
Normal file
44
packages/ai_chat_core/lib/src/services/channel_provider.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 一个通用的服务定位器,用于管理和提供所有类型的平台通道。
|
||||
class ChannelProvider {
|
||||
ChannelProvider._();
|
||||
|
||||
static final ChannelProvider instance = ChannelProvider._();
|
||||
|
||||
// 【修改】使用 Object 作为 value 类型,使其可以存储任何类型的 Channel
|
||||
final Map<String, Object> _channels = {};
|
||||
|
||||
/// 注册一个平台通道。
|
||||
/// 可以是 MethodChannel, EventChannel, 或 BasicMessageChannel。
|
||||
void register(String name, Object channel) {
|
||||
if (channel is! MethodChannel &&
|
||||
channel is! EventChannel &&
|
||||
channel is! BasicMessageChannel) {
|
||||
throw ArgumentError('The provided channel must be a MethodChannel, EventChannel, or BasicMessageChannel.');
|
||||
}
|
||||
_channels[name] = channel;
|
||||
}
|
||||
|
||||
/// 【修改】使用泛型方法获取一个已注册的平台通道,并进行类型检查。
|
||||
///
|
||||
/// 示例:
|
||||
/// final methodChannel = ChannelProvider.instance.get<MethodChannel>('my_method_channel');
|
||||
/// final eventChannel = ChannelProvider.instance.get<EventChannel>('my_event_channel');
|
||||
T? get<T extends Object>(String name) {
|
||||
final channel = _channels[name];
|
||||
if (channel is T) {
|
||||
return channel;
|
||||
}
|
||||
// 如果找到了通道但类型不匹配,可以打印一个警告
|
||||
if (channel != null) {
|
||||
print('Warning: Channel with name "$name" was found, but its type (${channel.runtimeType}) does not match the requested type ($T).');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 移除一个通道(可选功能)
|
||||
void unregister(String name) {
|
||||
_channels.remove(name);
|
||||
}
|
||||
}
|
||||
234
packages/ai_chat_core/lib/src/services/chat_sse_service.dart
Normal file
234
packages/ai_chat_core/lib/src/services/chat_sse_service.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../network/http_client.dart';
|
||||
import 'tts_service.dart';
|
||||
import '../utils/markdown_cleaner.dart';
|
||||
|
||||
class ChatSseService {
|
||||
// 依赖注入 TtsService
|
||||
final TtsService _ttsService;
|
||||
|
||||
// 缓存用户ID和会话ID
|
||||
String? _cachedUserId;
|
||||
String? _cachedConversationId;
|
||||
|
||||
// 使用我们封装的 HttpClient
|
||||
HttpClient? _currentClient;
|
||||
http.StreamedResponse? _currentResponse;
|
||||
StreamSubscription? _streamSubscription;
|
||||
|
||||
bool _isAborted = true;
|
||||
bool _isTtsStarted = false;
|
||||
|
||||
String? get conversationId => _cachedConversationId;
|
||||
|
||||
Future<void>? _startTtsFuture;
|
||||
|
||||
// 构造函数,接收 TtsService 实例
|
||||
ChatSseService(this._ttsService);
|
||||
|
||||
void request({
|
||||
required String messageId,
|
||||
required String text,
|
||||
required bool isChinese,
|
||||
required Function(String, String, bool) onStreamResponse,
|
||||
}) async {
|
||||
print("----------------------SSE Start");
|
||||
_isAborted = false;
|
||||
if (_cachedUserId == null) {
|
||||
_cachedUserId = _generateRandomUserId(6);
|
||||
print('初始化用户ID: $_cachedUserId');
|
||||
}
|
||||
String responseText = '';
|
||||
StringBuffer buffer = StringBuffer();
|
||||
final enEnders = RegExp(r'[.!?]');
|
||||
final zhEnders = RegExp(r'[。!?]');
|
||||
|
||||
// 【逻辑保留】processBuffer 的逻辑与你原来完全一致
|
||||
Future<void> processBuffer(bool isChinese) async {
|
||||
String txt = buffer.toString();
|
||||
// 先过滤 markdown 图片语法
|
||||
int imgStart = txt.indexOf('![');
|
||||
while (imgStart != -1) {
|
||||
int imgEnd = txt.indexOf(')', imgStart);
|
||||
if (imgEnd == -1) {
|
||||
// 图片语法未闭合,缓存剩余部分
|
||||
buffer.clear();
|
||||
buffer.write(txt.substring(imgStart));
|
||||
return;
|
||||
}
|
||||
// 移除图片语法
|
||||
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;
|
||||
int lastEnd = 0;
|
||||
// 本地缓存句子
|
||||
List<String> sentences = [];
|
||||
for (final match in enders.allMatches(txt)) {
|
||||
int endIndex = match.end;
|
||||
String sentence = txt.substring(lastEnd, endIndex).trim();
|
||||
if (sentence.isNotEmpty) {
|
||||
sentences.add(sentence);
|
||||
}
|
||||
lastEnd = endIndex;
|
||||
}
|
||||
// 只在达到完整句子时调用 TtsUtil.send
|
||||
for (final s in sentences) {
|
||||
// 【已迁移】使用 MarkdownCleaner 替代 CommonUtil
|
||||
String ttsStr = MarkdownCleaner.cleanText(s, true);
|
||||
// print("发送数据到TTS: $ttsStr");
|
||||
// 【已迁移】调用注入的 _ttsService
|
||||
_ttsService.send(ttsStr);
|
||||
}
|
||||
// 缓存剩余不完整部分
|
||||
String remain = txt.substring(lastEnd).trim();
|
||||
buffer.clear();
|
||||
if (remain.isNotEmpty) {
|
||||
buffer.write(remain);
|
||||
}
|
||||
}
|
||||
|
||||
_currentClient = HttpClient();
|
||||
try {
|
||||
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
|
||||
final request = http.Request('POST', chatUri)
|
||||
..headers['Content-Type'] = 'application/json'
|
||||
..headers['Accept'] = 'text/event-stream';
|
||||
|
||||
final body = {'message': text, 'user': _cachedUserId};
|
||||
if (_cachedConversationId != null) {
|
||||
body['conversation_id'] = _cachedConversationId;
|
||||
}
|
||||
request.body = json.encode(body);
|
||||
|
||||
_currentResponse = await _currentClient!.send(request);
|
||||
|
||||
if (_currentResponse!.statusCode == 200) {
|
||||
_startTtsFuture = startTts(isChinese);
|
||||
_streamSubscription = _currentResponse!.stream
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen(
|
||||
(line) {
|
||||
if (_isAborted) return;
|
||||
if (line.startsWith('data:')) {
|
||||
final jsonStr = line.substring(5).trim();
|
||||
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
|
||||
// 【已迁移】调用注入的 _ttsService
|
||||
_ttsService.complete();
|
||||
onStreamResponse(messageId, responseText, true);
|
||||
// 这里不需要 break,onDone 会被调用
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 【逻辑保留】这里是原来 await for 循环中的核心逻辑,已完整迁移
|
||||
final jsonData = json.decode(jsonStr);
|
||||
if (jsonData.containsKey('conversation_id') &&
|
||||
_cachedConversationId == null) {
|
||||
_cachedConversationId = jsonData['conversation_id'];
|
||||
}
|
||||
if (jsonData['event'].toString().contains('message')) {
|
||||
final textChunk =
|
||||
jsonData.containsKey('answer') ? jsonData['answer'] : '';
|
||||
if (_isAborted) {
|
||||
return;
|
||||
}
|
||||
buffer.write(textChunk);
|
||||
processBuffer(isChinese); // 注意:这里不再需要 await
|
||||
responseText += textChunk;
|
||||
onStreamResponse(messageId, responseText, false);
|
||||
}
|
||||
} catch (e) {
|
||||
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
print('SSE stream closed.');
|
||||
if (!_isAborted) {
|
||||
// 【已迁移】调用注入的 _ttsService
|
||||
_ttsService.complete();
|
||||
}
|
||||
// onDone 意味着流正常结束,也需要重置资源
|
||||
resetRequest();
|
||||
},
|
||||
onError: (error) {
|
||||
print('SSE stream error: $error');
|
||||
// onError 意味着流异常结束,需要重置资源
|
||||
resetRequest();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
} else {
|
||||
print('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
|
||||
// 【逻辑保留】连接失败也需要重置资源
|
||||
resetRequest();
|
||||
}
|
||||
} catch (e) {
|
||||
print('SSE request failed: $e');
|
||||
// 【逻辑保留】捕获到任何其他异常时,也需要重置资源
|
||||
resetRequest();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startTts(bool isChinese) async {
|
||||
print("----------------------TTS Start");
|
||||
if (!_isTtsStarted) {
|
||||
// 【已迁移】调用注入的 _ttsService
|
||||
if (await _ttsService.start(isChinese) == true) {
|
||||
_isTtsStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abort() async {
|
||||
_isAborted = true;
|
||||
// 【已迁移】调用注入的 _ttsService
|
||||
_ttsService.stop();
|
||||
resetRequest();
|
||||
}
|
||||
|
||||
void resetRequest() {
|
||||
// 【逻辑保留】重置资源,适配新的 HttpClient 和 StreamSubscription
|
||||
_streamSubscription?.cancel();
|
||||
_streamSubscription = null;
|
||||
|
||||
// package:http 的 client 只需要调用 close()
|
||||
_currentClient?.close();
|
||||
_currentClient = null;
|
||||
_currentResponse = null;
|
||||
_isTtsStarted = false;
|
||||
_startTtsFuture = null;
|
||||
}
|
||||
|
||||
// 【逻辑保留】_getCompleteTextEndIndex 方法保持不变
|
||||
int _getCompleteTextEndIndex(String buffer) {
|
||||
// 支持句号、问号、感叹号和换行符作为分割依据
|
||||
final sentenceEnders = RegExp(r'[,.!?:,。!?:\n]');
|
||||
final matches = sentenceEnders.allMatches(buffer);
|
||||
return matches.isEmpty ? 0 : matches.last.end;
|
||||
}
|
||||
|
||||
// 【逻辑保留】resetSession 方法保持不变
|
||||
void resetSession() {
|
||||
_cachedUserId = null;
|
||||
_cachedConversationId = null;
|
||||
print('SSE会话已重置');
|
||||
}
|
||||
|
||||
// 【逻辑保留】_generateRandomUserId 方法保持不变
|
||||
String _generateRandomUserId(int length) {
|
||||
const chars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
final random = Random();
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
|
||||
}
|
||||
}
|
||||
21
packages/ai_chat_core/lib/src/services/tts_service.dart
Normal file
21
packages/ai_chat_core/lib/src/services/tts_service.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// TTS 服务的抽象接口
|
||||
abstract class TtsService {
|
||||
|
||||
final String channelName;
|
||||
|
||||
TtsService(this.channelName);
|
||||
|
||||
/// 启动TTS
|
||||
Future<bool> start(bool isChinese);
|
||||
|
||||
/// 发送文本到TTS
|
||||
Future<void> send(String text);
|
||||
|
||||
/// 通知TTS所有文本已发送完毕
|
||||
Future<void> complete();
|
||||
|
||||
/// 停止TTS
|
||||
Future<void> stop();
|
||||
}
|
||||
46
packages/ai_chat_core/lib/src/utils/markdown_cleaner.dart
Normal file
46
packages/ai_chat_core/lib/src/utils/markdown_cleaner.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
class MarkdownCleaner {
|
||||
/// 清理文本中的 Markdown 语法
|
||||
static String cleanText(String text, bool forTts) {
|
||||
String cleanedText = text
|
||||
.replaceAllMapped(RegExp(r'\*\*(.*?)\*\*'), (m) => m.group(1) ?? '') // 粗体
|
||||
.replaceAllMapped(RegExp(r'\*(.*?)\*'), (m) => m.group(1) ?? '') // 斜体
|
||||
.replaceAllMapped(RegExp(r'`([^`]+)`'), (m) => m.group(1) ?? '') // 行内代码
|
||||
.replaceAll(RegExp(r'```[^`]*```', multiLine: true), '') // 完整代码块
|
||||
.replaceAll(RegExp(r'```[^`]*$', multiLine: true), '') // 不完整代码块开始
|
||||
.replaceAll(RegExp(r'^[^`]*```', multiLine: true), '') // 不完整代码块结束
|
||||
.replaceAll(RegExp(r'^\s*\|.*\|\s*$', multiLine: true), '') // 表格行
|
||||
.replaceAll(RegExp(r'^\s*[\|\-\:\+\=\s]+\s*$', multiLine: true), '') // 表格分隔符
|
||||
.replaceAll(RegExp(r'\|'), ' ') // 清理残留的竖线
|
||||
.replaceAllMapped(RegExp(r'^#{1,6}\s+(.*)$', multiLine: true), (m) => m.group(1) ?? '')
|
||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^\)]+\)'), '') // 完整超链接
|
||||
.replaceAll(RegExp(r'\[([^\]]*)\](?!\()'), r'$1') // 只有方括号
|
||||
.replaceAll(RegExp(r'\]\([^\)]*\)'), '') // 只有圆括号
|
||||
.replaceAll(RegExp(r'!\[([^\]]*)\]\([^\)]+\)'), '') // 图片链接
|
||||
.replaceAllMapped(RegExp(r'^>\s*(.*)$', multiLine: true), (m) => m.group(1) ?? '')
|
||||
.replaceAllMapped(RegExp(r'^\s*[–—\-*+-]+\s*(.*)$', multiLine: true), (m) => m.group(1) ?? '')
|
||||
.replaceAllMapped(RegExp(r'^\s*\d+\.\s+(.*)$', multiLine: true), (m) => m.group(1) ?? '')
|
||||
.replaceAll(RegExp(r'^\s*[-*]{3,}\s*$', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'\*+'), '') // 清理残留星号
|
||||
.replaceAll(RegExp(r'`+'), '') // 清理残留反引号
|
||||
.replaceAll(RegExp(r'#+'), '') // 清理残留井号
|
||||
.replaceAll(RegExp(r'\n\s*\n'), '\n')
|
||||
.replaceAll(RegExp(r'\n'), ' ')
|
||||
.replaceAll(RegExp(r'!\$\d'), '') // 清理占位符
|
||||
.replaceAll(RegExp(r'\$\d+'), '') // 清理所有 $数字 占位符
|
||||
.trim();
|
||||
|
||||
if (forTts) {
|
||||
if (cleanedText.contains("ID.UNYX")) {
|
||||
cleanedText = cleanedText.replaceAllMapped(
|
||||
RegExp(r'ID\.UNYX', caseSensitive: false), (m) => 'ID Unix');
|
||||
} else {
|
||||
cleanedText = cleanedText.replaceAllMapped(
|
||||
RegExp(r'UNYX', caseSensitive: false), (m) => 'Unix');
|
||||
}
|
||||
cleanedText = cleanedText.replaceAllMapped(
|
||||
RegExp(r'400-023-4567', caseSensitive: false), (m) => '4 0 0 - 0 2 3 - 4 5 6 7');
|
||||
}
|
||||
|
||||
return cleanedText;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@ packages:
|
||||
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"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -59,6 +67,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
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"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -168,6 +192,22 @@ packages:
|
||||
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: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -184,6 +224,14 @@ packages:
|
||||
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"
|
||||
sdks:
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
||||
@@ -10,6 +10,9 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
http: ^1.5.0
|
||||
uuid: ^3.0.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@@ -16,4 +16,3 @@ class MethodChannelFlutterVoskWakeword extends FlutterVoskWakewordPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user