You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

145 lines
5.9 KiB

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<Color> gradient;
/// 是否依赖设备连接:未连接时卡片置灰,点击提示连接设备
final bool requiresDevice;
/// 尚未实现的占位模块:可以加到 AI 页排版,但点击只提示"敬请期待"
final bool comingSoon;
/// 这个模块用哪个百炼应用(app_id)。null = 用全局默认(EMAI 那个)。
///
/// **设备按键唤醒时用它决定接哪个助手**:`DeviceAiSessionService` 按
/// 厂商 → 品类 → 默认编排第一个模块 → 这个字段,找到该说话的那个 agent。
/// 车载香薰唤醒的是「Smartcar 车载助手」而不是 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';
/// Smartcar 车载助手(车载香薰的对话 agent,另一个百炼应用)
static const String smartcarChat = 'smartcarChat';
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<AgentModule> all = const [];
/// AI 页首次进入时的默认模块。
///
/// 没连过设备也能用的三个(都只依赖手机本身):语音记录助手、EMAI助手、现场录音。
/// 依赖设备的功能等连上对应设备后由 [defaultsForCategory] 自动补进来。
static const List<String> defaultEnabled = [
// 网格区(左大 + 右上下两小)
call,
simultaneous,
faceToFace,
// 并排方块
meeting,
audioVideo,
// 往下是整行横条,现场录音是第一行
fieldRecording,
callRecording,
textTranslation,
pictureTranslation,
emaiAssistant,
];
/// 各设备品类**第一次连接成功**时要铺到 AI 页的默认功能与顺序。
///
/// key 用的是 [DeviceCategories] 的品类 id。只在该品类首次连接时生效一次
/// (由 AgentModuleService 记录已应用过的品类),之后用户怎么增删排序都不再覆盖。
static const Map<String, List<String>> defaultsByCategory = {
// AI手机支架(EaiRec):连上后 AI 页默认就这三个 —— 语音纪要 / 现场录音 /
// EMAI助手。**不含通话录音**:支架没有接管通话音频的能力,摆在这儿点进去
// 也用不了。
DeviceCategories.phoneHolder: [meeting, fieldRecording, emaiAssistant],
// AI录音卡片是另一条产品线,保持原编排不动。
DeviceCategories.noteCard: [meeting, fieldRecording, callRecording],
// AI耳机(恒玄):通话 + 翻译整套都能跑,铺开全量默认编排
DeviceCategories.earphone: defaultEnabled,
// AI车载香薰(Smartcar):随时开聊 + 现场录音 + 语音纪要。
// 这三个都不依赖设备能力(只有通话录音/翻译才声明 requiredCapabilities),
// 所以连上香薰就能直接用,不会灰。
// ⚠️ 这是**默认编排不是权限限制**——用户仍可去广场把别的功能加回来,
// 这是产品有意接受的(落地文档 6.5)。
// ⚠️ 2026-09-11 从「只有 smartcarChat」改成这三个;已经铺过旧编排的装机
// 由 AgentModuleService 的一次性补铺处理,见那边的 _migrateCarAromaDefaults。
DeviceCategories.carAroma: [smartcarChat, fieldRecording, meeting],
};
static List<String> 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;
}
}