import 'package:flutter/material.dart'; import 'device_category.dart'; /// 功能模块(agent)定义。 /// /// AI 页与广场页共用同一份注册表:广场展示全部,AI 页只展示用户启用的那些, /// 顺序由 [AgentModuleService] 持久化。新增 agent 见 `lib/modules/module_registry.dart`。 class AgentModule { const AgentModule({ required this.id, required this.title, required this.icon, required this.gradient, this.subtitle, this.requiresDevice = false, this.comingSoon = false, this.bailianAppId, }); /// 唯一标识,持久化时存的就是它,改名会导致用户已有配置失效 final String id; /// i18n key,展示时 `.tr`(不是中文文案本身) final String title; /// i18n key,展示时 `.tr` final String? subtitle; final IconData icon; /// 卡片图标底色渐变(两个色值) final List gradient; /// 是否依赖设备连接:未连接时卡片置灰,点击提示连接设备 final bool requiresDevice; /// 尚未实现的占位模块:可以加到 AI 页排版,但点击只提示"敬请期待" final bool comingSoon; /// 这个模块用哪个百炼应用(app_id)。null = 用全局默认(EMAI 那个)。 /// /// **设备按键唤醒时用它决定接哪个助手**:`DeviceAiSessionService` 按 /// 厂商 → 品类 → 默认编排第一个模块 → 这个字段,找到该说话的那个 agent。 /// 车载香薰唤醒的是「EaiCar 车载助手」而不是 EMAI,就是靠这一条。 /// /// ⚠️ 放在 data 层的基类而不是 `AgentModuleDescriptor`(modules 层),是为了让 /// `DeviceAiSessionService`(data 层)读得到 —— **data 层不反向依赖 modules 层**。 final String? bailianAppId; } /// 按模块 id 取它要接的百炼 app_id。查不到返回 null(用全局默认)。 String? bailianAppIdForModule(String? moduleId) { if (moduleId == null) return null; for (final m in AgentModules.all) { if (m.id == moduleId) return m.bailianAppId; } return null; } /// 全部可用模块的注册表 class AgentModules { AgentModules._(); // ---------- 已实现(对应现有路由) ---------- static const String meeting = 'meeting'; static const String simultaneous = 'simultaneous'; static const String faceToFace = 'faceToFace'; static const String call = 'call'; static const String audioVideo = 'audioVideo'; static const String callRecording = 'callRecording'; static const String fieldRecording = 'fieldRecording'; /// EMAI 助手:原来 AI 页底部那颗悬浮球,改成广场里的一个正式 agent static const String emaiAssistant = 'emaiAssistant'; /// EaiCar 车载助手(车载香薰的对话 agent,另一个百炼应用) static const String eaicarChat = 'smartcarChat'; // 已铺进装机编排的模块 id,改名后值不动 static const String textTranslation = 'textTranslation'; static const String pictureTranslation = 'pictureTranslation'; // ---------- 规划中(占位,方便测试增删) ---------- static const String englishChat = 'englishChat'; static const String englishStudy = 'englishStudy'; static const String kidsStory = 'kidsStory'; /// 全部模块。由 `ModuleRegistry.install()` 在启动时注入——注册表本体在 /// `lib/modules/module_registry.dart`(描述符要引用各模块的页面,只能放在 /// modules 层;data 层不反向依赖它)。 static List all = const []; /// AI 页首次进入时的默认模块。 /// /// 没连过设备也能用的三个(都只依赖手机本身):语音记录助手、EMAI助手、现场录音。 /// 依赖设备的功能等连上对应设备后由 [defaultsForCategory] 自动补进来。 static const List defaultEnabled = [ // 网格区(左大 + 右上下两小) call, simultaneous, faceToFace, // 并排方块 meeting, audioVideo, // 往下是整行横条,现场录音是第一行 fieldRecording, callRecording, textTranslation, pictureTranslation, emaiAssistant, ]; /// 各设备品类**第一次连接成功**时要铺到 AI 页的默认功能与顺序。 /// /// key 用的是 [DeviceCategories] 的品类 id。只在该品类首次连接时生效一次 /// (由 AgentModuleService 记录已应用过的品类),之后用户怎么增删排序都不再覆盖。 static const Map> defaultsByCategory = { // AI手机支架(EaiRec):连上后 AI 页默认就这三个 —— 语音纪要 / 现场录音 / // EMAI助手。**不含通话录音**:支架没有接管通话音频的能力,摆在这儿点进去 // 也用不了。 DeviceCategories.phoneHolder: [meeting, fieldRecording, emaiAssistant], // AI录音卡片是另一条产品线,保持原编排不动。 DeviceCategories.noteCard: [meeting, fieldRecording, callRecording], // AI耳机(恒玄):通话 + 翻译整套都能跑,铺开全量默认编排 DeviceCategories.earphone: defaultEnabled, // AI车载香薰(EaiCar):随时开聊 + 现场录音 + 语音纪要。 // 这三个都不依赖设备能力(只有通话录音/翻译才声明 requiredCapabilities), // 所以连上香薰就能直接用,不会灰。 // ⚠️ 这是**默认编排不是权限限制**——用户仍可去广场把别的功能加回来, // 这是产品有意接受的(落地文档 6.5)。 // ⚠️ 2026-09-11 从「只有 eaicarChat」改成这三个;已经铺过旧编排的装机 // 由 AgentModuleService 的一次性补铺处理,见那边的 _migrateCarAromaDefaults。 DeviceCategories.carAroma: [eaicarChat, fieldRecording, meeting], }; static List defaultsForCategory(String? categoryId) { if (categoryId == null) return const []; return defaultsByCategory[categoryId] ?? const []; } static AgentModule? byId(String id) { for (final m in all) { if (m.id == id) return m; } return null; } }