import 'dart:convert'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../core/utils/logger.dart'; import '../models/emai_message.dart'; /// EMAI 助手的对话记录,手机页面和耳机会话**共用同一份**。 /// /// 之所以要独立成一个常驻服务而不是挂在页面控制器上:耳机按键唤醒的对话 /// 发生时页面很可能根本没打开(人在路上、手机在兜里)。用户要求这些内容 /// 在打开界面时也能看到,所以记录必须活在页面之外。 /// /// 落盘用 GetStorage,只保留最近 [_maxKeep] 条。 /// /// ⚠️ 注意语义:这里存的是**展示用的聊天记录**,不是模型上下文。 /// 百炼的上下文按 `dialog_id` 维护在服务端,每次重新连接就是一轮新对话—— /// 也就是说界面上能看到旧消息,但模型并不记得它们。 class EmaiChatStore extends GetxService { static const String _tag = 'EmaiChatStore'; static EmaiChatStore get to => Get.find(); static const String _storageKey = 'emai_chat_history'; static const int _maxKeep = 200; final GetStorage _storage = GetStorage(); final messages = [].obs; @override void onInit() { super.onInit(); _load(); } void _load() { try { final raw = _storage.read(_storageKey); if (raw == null || raw.isEmpty) return; final list = json.decode(raw) as List; messages .assignAll(list.map((e) => EmaiMessage.fromJson(e as Map)).toList()); Logger.i(_tag, '恢复历史对话 ${messages.length} 条'); } catch (e) { Logger.w(_tag, '历史对话读取失败,丢弃: $e'); _storage.remove(_storageKey); } } /// 落盘。流式过程中每帧都写太浪费,只在一句话收完时调。 void persist() { try { final keep = messages.length > _maxKeep ? messages.sublist(messages.length - _maxKeep) : messages; _storage.write( _storageKey, json.encode(keep.map((m) => m.toJson()).toList())); } catch (e) { Logger.w(_tag, '历史对话落盘失败: $e'); } } EmaiMessage append(EmaiMessage m) { messages.add(m); return m; } /// 流式文本一律整体替换,不做拼接——理由见 EmaiController._applyText void applyText(EmaiMessage m, String incoming) { if (incoming.isEmpty) return; m.text = incoming; messages.refresh(); } /// 一个字都没收到的流式气泡:从列表里撤掉,别留白气泡 void dropIfEmpty(EmaiMessage? m) { if (m == null) return; if (m.text.trim().isEmpty) messages.remove(m); } void finalize(EmaiMessage? m) { if (m == null) return; m.isFinal = true; dropIfEmpty(m); messages.refresh(); persist(); } void clear() { messages.clear(); _storage.remove(_storageKey); } }