feat: 新增ai_chat_core,将core 和 widget 分离(未完成)
迁移了 models enums, utils, http 封装,还有一些extensions;service 只迁移了 sse service
This commit is contained in:
@@ -16,14 +16,7 @@ export 'services/message_service.dart';
|
||||
export 'services/command_service.dart';
|
||||
|
||||
// types && enums && models
|
||||
export 'models/vehicle_cmd.dart';
|
||||
export 'models/vehicle_cmd_response.dart';
|
||||
export 'models/chat_message.dart';
|
||||
export 'models/vehicle_status_info.dart';
|
||||
|
||||
export 'enums/vehicle_command_type.dart';
|
||||
export 'enums/message_status.dart';
|
||||
export 'enums/message_service_state.dart';
|
||||
export 'package:ai_chat_core/ai_chat_core.dart';
|
||||
|
||||
// utils
|
||||
export 'utils/assets_util.dart';
|
||||
@@ -1,5 +1,3 @@
|
||||
import '../enums/vehicle_command_type.dart';
|
||||
import '../services/command_service.dart';
|
||||
import 'easy_bloc.dart';
|
||||
import 'command_state.dart';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../enums/vehicle_command_type.dart';
|
||||
|
||||
// AI Chat Command States
|
||||
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||
|
||||
enum AIChatCommandStatus {
|
||||
idle,
|
||||
executing,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
enum MessageServiceState {
|
||||
idle,
|
||||
recording,
|
||||
recognizing,
|
||||
replying,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -10,7 +11,6 @@ import 'package:porcupine_flutter/porcupine_manager.dart';
|
||||
|
||||
import 'bloc/ai_chat_cubit.dart';
|
||||
import 'bloc/command_state.dart';
|
||||
import 'models/vehicle_cmd.dart';
|
||||
|
||||
/// 车辆命令处理器抽象类
|
||||
abstract class VehicleCommandHandler {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:ai_chat_assistant/models/chat_message.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';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ai_chat_assistant/utils/tts_util.dart';
|
||||
import '../utils/tts_util.dart';
|
||||
|
||||
import '../utils/common_util.dart';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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 {
|
||||
|
||||
@@ -8,14 +8,17 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../services/chat_sse_service.dart';
|
||||
import '../services/chat_sse_service.dart' as Service;
|
||||
import '../services/classification_service.dart';
|
||||
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/ali_sdk');
|
||||
@@ -77,7 +80,9 @@ class MessageService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
36
lib/services/platform_tts_service.dart
Normal file
36
lib/services/platform_tts_service.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// TtsService 的平台特定实现,通过 MethodChannel 调用原生代码
|
||||
class PlatformTtsService implements TtsService {
|
||||
@override
|
||||
final String channelName;
|
||||
|
||||
final MethodChannel _channel;
|
||||
|
||||
PlatformTtsService(this.channelName): _channel = MethodChannel(channelName);
|
||||
|
||||
Future<T?> _execute<T>(String method, [Map<Object, Object>? arguments]) {
|
||||
return _channel.invokeMethod(method, arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> start(bool isChinese) async {
|
||||
return await _execute('startTts', {'isChinese': isChinese}) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send(String text) async {
|
||||
await _execute('sendTts', {'text': text});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> complete() async {
|
||||
await _execute('completeTts');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
await _execute('stopTts');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:ai_chat_assistant/utils/common_util.dart';
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/chat_message.dart';
|
||||
import 'chat_bubble.dart';
|
||||
|
||||
class ChatBox extends StatelessWidget {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:ai_chat_assistant/utils/common_util.dart';
|
||||
import 'package:ai_chat_assistant/widgets/rotating_image.dart';
|
||||
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||
import 'package:t_basic_intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||
import '../enums/message_status.dart';
|
||||
import '../models/chat_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/message_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Message状态枚举
|
||||
enum MessageStatus {
|
||||
normal('普通消息', 'Normal'),
|
||||
listening('聆听中', 'Listening'),
|
||||
@@ -13,4 +14,4 @@ enum MessageStatus {
|
||||
|
||||
final String chinese;
|
||||
final String english;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:ai_chat_assistant/models/vehicle_cmd.dart';
|
||||
import 'vehicle_cmd.dart';
|
||||
|
||||
class VehicleCommandResponse {
|
||||
final String? tips;
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ dependencies:
|
||||
meta: ^1.15.0
|
||||
fluttertoast: ^8.2.12
|
||||
record: ^6.0.0
|
||||
http: ^1.4.0
|
||||
http: ^1.5.0
|
||||
path_provider: ^2.1.5
|
||||
flutter_markdown : ^0.7.7
|
||||
flutter_markdown_plus : ^1.0.1
|
||||
|
||||
Reference in New Issue
Block a user