import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../core/utils/logger.dart'; import '../models/assistant_directive.dart'; /// 助手指令的落地层:**解析 → 分发 → 落本地记录**。 /// /// 指令由 [BailianMultimodalService] 从 `RespondingContent.extra_info.tool_calls` /// 解出来,App 内对话页(EmaiController)和耳机唤醒(BesAiSessionService)两条 /// 链路都会往这里送,这里是唯一的汇合点。 /// /// 目前只有 `SET_clock` 真正落地(落成提醒,展示在「代办」页)。其余指令一律 /// 记一条 `unsupported` 流水——**不是丢掉**:将来要按这份流水看用户到底在用 /// 哪些能力、以及百炼后台配了但客户端没实现的有哪些。 /// /// 两份数据分开存: /// - [directives] 是流水日志(含不支持、失败的),只增不改,用于回溯与将来上传; /// - [reminders] 是业务实体(用户会看会勾会删),代办页读它。 /// /// ⚠️ 目前只在**本地**存。服务端同步接口尚未开发,两个模型的 `synced` 字段和 /// `toJson()` 已按可直接上传的形状设计好,接口就绪后补上传即可,不用改结构。 /// /// ⚠️ 另一个已知边界:指令是从 EMAI 的 WebSocket 会话里回来的,而会话只在 /// 对话页打开(或耳机唤醒)时才建立。要做到「App 没打开也能定闹钟」,得先有 /// 常驻的会话持有者,光把执行代码挪到原生层不解决这个问题。 class AssistantDirectiveService extends GetxService { static AssistantDirectiveService get to => Get.find(); static const String _tag = 'AssistantDirective'; static const String _kDirectives = 'assistant_directives'; static const String _kReminders = 'assistant_reminders'; /// 流水最多留这么多条,超了丢最旧的——它只是排查/上传用的日志,不是账本 static const int _maxDirectives = 300; final GetStorage _storage = GetStorage(); /// 指令流水,新的在前 final RxList directives = [].obs; /// 提醒(代办页的数据源),按时间正序 final RxList reminders = [].obs; /// 本版明确不做的指令:上一轮已经把电话/联系人能力整个删掉了 /// (权限、清单声明、原生实现都删了),这两条即使接上也执行不了。 static const Map _declined = { 'SEND_message': '本版已移除短信与联系人能力', 'MAKE_A_PHONE_CALL_phone_call': '本版已移除拨号与联系人能力', }; @override void onInit() { super.onInit(); _restore(); } /// 收到一批指令:逐条分发并落记录。 /// /// 同一 `round_id` 重复送达时按 id 去重——耳机与对话页可能同时在监听同一个 /// 会话,重复执行会把同一个闹钟定两遍。 void handle(List incoming) { if (incoming.isEmpty) return; var changed = false; for (final d in incoming) { if (directives.any((e) => e.id == d.id)) { Logger.d(_tag, '忽略重复指令 ${d.id}'); continue; } _dispatch(d); directives.insert(0, d); changed = true; Logger.i(_tag, '指令落地 $d'); } if (!changed) return; if (directives.length > _maxDirectives) { directives.removeRange(_maxDirectives, directives.length); } _persist(); } void _dispatch(AssistantDirective d) { final declined = _declined[d.name]; if (declined != null) { d.status = DirectiveStatus.unsupported; d.note = declined; return; } switch (d.name) { case 'SET_clock': _handleSetClock(d); break; default: // 已知但本版没接的:ROUTE_map(导航)、INCREASE_DEFAULT_volume(音量)… // 以及百炼后台后加的任何新指令。留一条流水,不报错。 d.status = DirectiveStatus.unsupported; d.note = '尚未实现的指令'; } } void _handleSetClock(AssistantDirective d) { final reminder = AssistantReminder.fromSetClock(d); if (reminder == null) { d.status = DirectiveStatus.failed; d.note = 'time 缺失或格式无法解析: ${d.rawArguments}'; Logger.w(_tag, 'SET_clock 解析失败: ${d.rawArguments}'); return; } reminders.add(reminder); _sortReminders(); d.status = DirectiveStatus.done; d.note = reminder.repeat == RepeatRule.unknown && reminder.repeatRaw.isNotEmpty ? '重复规则「${reminder.repeatRaw}」未识别,按单次处理' : null; } /// 用户在代办页勾选完成 void toggleReminderDone(String id, bool done) { final r = reminders.firstWhereOrNull((e) => e.id == id); if (r == null) return; r.done = done; reminders.refresh(); _persist(); } void removeReminder(String id) { reminders.removeWhere((e) => e.id == id); _persist(); } /// 排查用:清空本地记录 void clearAll() { directives.clear(); reminders.clear(); _persist(); } void _sortReminders() { reminders.sort((a, b) { final ad = a.date ?? ''; final bd = b.date ?? ''; final byDate = ad.compareTo(bd); return byDate != 0 ? byDate : a.time.compareTo(b.time); }); } void _restore() { try { final rawD = _storage.read(_kDirectives) ?? const []; directives.assignAll(rawD .whereType() .map((e) => AssistantDirective.fromJson(e.cast()))); final rawR = _storage.read(_kReminders) ?? const []; reminders.assignAll(rawR .whereType() .map((e) => AssistantReminder.fromJson(e.cast()))); _sortReminders(); Logger.i(_tag, '恢复本地记录: ${directives.length} 条指令 / ${reminders.length} 条提醒'); } catch (e) { // 存档格式对不上(比如改过模型字段)不该让服务起不来,丢掉重来即可 Logger.e(_tag, '恢复本地记录失败,已重置: $e'); directives.clear(); reminders.clear(); } } void _persist() { try { _storage.write(_kDirectives, directives.map((e) => e.toJson()).toList()); _storage.write(_kReminders, reminders.map((e) => e.toJson()).toList()); } catch (e) { Logger.e(_tag, '写本地记录失败: $e'); } } }