feat: 新增ai_chat_core,将core 和 widget 分离(未完成)

迁移了 models enums, utils, http 封装,还有一些extensions;service 只迁移了 sse service
This commit is contained in:
2025-09-29 18:33:46 +08:00
parent ba73d6d780
commit a7ed838c92
40 changed files with 933 additions and 38 deletions

View File

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

View File

@@ -1,5 +1,3 @@
import '../enums/vehicle_command_type.dart';
import '../services/command_service.dart';
import 'easy_bloc.dart';
import 'command_state.dart';

View File

@@ -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,

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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();

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

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

View File

@@ -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 {

View File

@@ -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 {

View File

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

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

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

View File

@@ -0,0 +1,11 @@
/// 消息服务状态
enum MessageServiceState {
// 空闲
idle,
// 录音中
recording,
// 识别中
recognizing,
// 回复中
replying,
}

View File

@@ -1,3 +1,4 @@
// Message状态枚举
enum MessageStatus {
normal('普通消息', 'Normal'),
listening('聆听中', 'Listening'),
@@ -13,4 +14,4 @@ enum MessageStatus {
final String chinese;
final String english;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
import 'dart:ui';
import 'package:flutter/material.dart';
extension ColorExtension on Color {
static const Color commonColor = Color(0xFF6F72F1);
}

View File

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

View File

@@ -1,4 +1,4 @@
import 'package:ai_chat_assistant/models/vehicle_cmd.dart';
import 'vehicle_cmd.dart';
class VehicleCommandResponse {
final String? tips;

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

View File

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

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

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

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

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

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

View 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);
// 这里不需要 breakonDone 会被调用
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))));
}
}

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

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

View File

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

View File

@@ -10,6 +10,9 @@ dependencies:
flutter:
sdk: flutter
http: ^1.5.0
uuid: ^3.0.5
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -16,4 +16,3 @@ class MethodChannelFlutterVoskWakeword extends FlutterVoskWakewordPlatform {
}
}

View File

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