Browse Source
- 设备名闸门:支架只认 EaiRec/EAIMAR/Pad Note,车载香薰只认 EaiCar,固件名对不上直接不连; 一次性清掉此前被归一化名字污染的配对记录;Smartcar 资源与 agent 命名统一改为 EaiCar - 绑定登记:新增 DeviceBindRegistry(按 uid|MAC 去重),支架也进 verifyConnected, 登录态从无到有时补报一次,换账号不再漏切绑定 - iOS 崩溃加固:补 NSPhotoLibraryAddUsageDescription(保存思维导图/统计图必崩); StsAgent 先配音频会话再 installTap 并校验格式;支架录音两处 installTap 加 0Hz 守卫; AzureTtsHelper 播放队列改加锁队列(三线程裸改 Array,release 下崩); MicrophoneCapture 主线程不再用信号量等权限弹窗(看门狗误杀) - 网络:connectionError 给中文文案;实时翻译请求 12s 超时,不再跟 30s 默认值等 - 附:AI 助手中枢设计文档 v0.2(未动代码) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>main
27 changed files with 1113 additions and 90 deletions
@ -0,0 +1,85 @@ |
|||
import 'dart:async'; |
|||
|
|||
import '../core/utils/logger.dart'; |
|||
import '../data/models/user_Info.dart'; |
|||
import '../data/services/network/api.dart'; |
|||
|
|||
/// 「这台设备现在属于哪个账号」的登记。 |
|||
/// |
|||
/// ⚠️ 与**确权**(这台硬件是不是正品)是两件完全不同的事,必须分开: |
|||
/// - 确权是**设备属性**,可以缓存在本地白名单里,离线也认,一台正品永远是正品; |
|||
/// - 登记是**账号属性**,同一台设备换个账号登录就得重新报一次。 |
|||
/// |
|||
/// 把两者混在一条路径上(白名单命中就整段 return)会导致一个不报错的坑: |
|||
/// A 账号绑过的设备,换 B 账号登录后连上时 `user_binddevice` **一次都不打**, |
|||
/// 服务端仍认为它属于 A —— B 的「设备管理」页永远空着,而连接、录音、OTA 全都正常, |
|||
/// 看不出任何异常。设备在同事之间来回借用、或同一台手机切账号测试时必然踩到。 |
|||
/// |
|||
/// 服务端本来就支持接管:`api_binddevice.go` 里 |
|||
/// `model.Status != 0 && model.Uid != session.GetUserId()` 那一支会删掉原账号的 |
|||
/// userdevice 行、把绑定统计冲正 -1,再把设备行改挂到新账号名下 |
|||
/// (「授权码已被使用」的拒绝早就注释掉了)。**客户端只要把「连上了」这件事告诉它**。 |
|||
class DeviceBindRegistry { |
|||
DeviceBindRegistry._(); |
|||
|
|||
static const String _tag = 'DeviceBindRegistry'; |
|||
|
|||
/// 本进程内已向服务端报告过的 `uid|MAC`,避免同一次登录里反复打接口。 |
|||
/// |
|||
/// ⚠️ 键**必须带 uid**:只按 MAC 记会把「换账号」这件事连同一起吞掉, |
|||
/// 那就等于又回到了白名单短路的老问题上。 |
|||
static final Set<String> _reported = <String>{}; |
|||
|
|||
/// 登出时清掉:换账号后同一台设备要重新登记到新账号名下。 |
|||
static void reset() => _reported.clear(); |
|||
|
|||
/// 确保**当前账号**在服务端登记了这台设备。 |
|||
/// |
|||
/// 调用方不必 await —— 连接流程不该等这一次网络往返,登记失败也不影响设备可用。 |
|||
/// 未登录时直接返回:那条路由 [DeviceAuth.bindLoginRetry] 在登录完成后补。 |
|||
static Future<void> ensureBound({ |
|||
required String mac, |
|||
required String name, |
|||
}) async { |
|||
final m = mac.trim().toUpperCase(); |
|||
if (m.isEmpty) return; |
|||
if (!User.isLoggedIn()) return; |
|||
|
|||
final uid = User.instance.uid; |
|||
// 服务端已经知道这台属于当前账号 —— 省一次往返。 |
|||
// 这个判据天然按账号走(devices 是当前账号的列表),不需要额外缓存。 |
|||
if (User.instance.devices |
|||
.any((d) => d.devicemac.trim().toUpperCase() == m)) { |
|||
return; |
|||
} |
|||
|
|||
final key = '$uid|$m'; |
|||
if (_reported.contains(key)) return; |
|||
_reported.add(key); // 先占位挡住并发;失败时移除,下次连上还能重试 |
|||
try { |
|||
final resp = await Api.binddevice({ |
|||
'code': '', // BLE 设备没有杰理那套广播授权码,服务端按 MAC 全局反查 |
|||
'devicename': name, |
|||
'devicemac': mac, |
|||
}).timeout(const Duration(seconds: 10)); |
|||
final deviceJson = resp?['device']; |
|||
if (deviceJson is Map<String, dynamic>) { |
|||
User.instance.addUserDevice(UserDevice.fromJson(deviceJson)); |
|||
Logger.i(_tag, '已登记到当前账号($uid): $m'); |
|||
return; |
|||
} |
|||
// resp == null 是服务端**明确拒绝**(AuthInterceptor 在业务码 != 0 时 |
|||
// 把 data 置 null 后 resolve,不抛)。最常见是这台 MAC 还没导入后台的 |
|||
// device_mac 表,返回 {"code":2005,"msg":"设备未登记"}。 |
|||
_reported.remove(key); |
|||
Logger.w( |
|||
_tag, |
|||
'服务端未返回 device(resp=$resp):这台 MAC 可能没导入 device_mac,' |
|||
'设备管理页看不到它'); |
|||
} catch (e) { |
|||
// 网络问题:不记成功,下次连上重试。登记失败不影响连接与使用。 |
|||
_reported.remove(key); |
|||
Logger.w(_tag, '登记失败(不影响使用): $e'); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
import 'package:eaimar/devices/holder/holder_device_service.dart'; |
|||
import 'package:flutter_test/flutter_test.dart'; |
|||
|
|||
void main() { |
|||
group('isAllowedHolderName —— 广播名准入校验', () { |
|||
// 名字对不上的固件不是正式产品,服务端的 MAC 校验同样过不了, |
|||
// 连上只会让用户以为设备可用、然后在设备管理页找不到它。 |
|||
test('规定的名字放行', () { |
|||
expect(isAllowedHolderName('EaiRec'), isTrue); |
|||
expect(isAllowedHolderName('EAIMAR'), isTrue); |
|||
expect(isAllowedHolderName('Pad Note'), isTrue); |
|||
}); |
|||
|
|||
test('大小写与空白不影响判定(固件写法不一定规范)', () { |
|||
expect(isAllowedHolderName(' eairec '), isTrue); |
|||
expect(isAllowedHolderName('EAIREC'), isTrue); |
|||
}); |
|||
|
|||
test('带批次后缀的广播名放行', () { |
|||
expect(isAllowedHolderName('EaiRec-3F2A'), isTrue); |
|||
}); |
|||
|
|||
// ⚠️ 这条是本次改动的重点:旧固件名不再被接纳,也不做归一映射。 |
|||
// 归一等于接纳不合规固件、把失败推到服务端再发生一次。 |
|||
test('旧固件名 Echomeet 一律拒绝', () { |
|||
expect(isAllowedHolderName('Echomeet'), isFalse); |
|||
expect(isAllowedHolderName('echomeet'), isFalse); |
|||
expect(isAllowedHolderName('Echomeet-3F2A'), isFalse); |
|||
}); |
|||
|
|||
test('空名字拒绝,不要放行', () { |
|||
expect(isAllowedHolderName(''), isFalse); |
|||
expect(isAllowedHolderName(' '), isFalse); |
|||
}); |
|||
|
|||
test('无关设备拒绝', () { |
|||
expect(isAllowedHolderName('Echo-one'), isFalse); |
|||
expect(isAllowedHolderName('Smartcar'), isFalse); |
|||
}); |
|||
}); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
import 'package:eaimar/devices/smartcar/smartcar_link.dart'; |
|||
import 'package:flutter_test/flutter_test.dart'; |
|||
|
|||
void main() { |
|||
group('isAllowedSmartcarName —— 车载香薰广播名准入', () { |
|||
test('规定的名字放行', () { |
|||
expect(isAllowedSmartcarName('EaiCar'), isTrue); |
|||
expect(isAllowedSmartcarName(' eaicar '), isTrue); |
|||
expect(isAllowedSmartcarName('EAICAR-3F2A'), isTrue); |
|||
}); |
|||
|
|||
// ⚠️ 旧广播名不再接纳。厂商 ID(0x8505) 也不再单独放行——它只说明 |
|||
// "这是云衍方案的设备",说明不了"这是我们的正式产品"。 |
|||
test('旧名 Smartcar 一律拒绝', () { |
|||
expect(isAllowedSmartcarName('Smartcar'), isFalse); |
|||
expect(isAllowedSmartcarName('smartcar'), isFalse); |
|||
expect(isAllowedSmartcarName('SmartcarX'), isFalse); |
|||
}); |
|||
|
|||
test('空名字拒绝', () { |
|||
expect(isAllowedSmartcarName(''), isFalse); |
|||
expect(isAllowedSmartcarName(' '), isFalse); |
|||
}); |
|||
|
|||
test('别家设备拒绝', () { |
|||
expect(isAllowedSmartcarName('EaiRec'), isFalse); |
|||
expect(isAllowedSmartcarName('Echo-one'), isFalse); |
|||
}); |
|||
}); |
|||
} |
|||
@ -0,0 +1,602 @@ |
|||
# AI 助手中枢:翻译入纪要 · 纪要入拾忆 · EMAI 综合入口 —— 设计与落地文档 |
|||
|
|||
> 版本:v0.2 日期:2026-09-15(v0.1 同日草案;v0.2 按现有服务器架构复核:五进程同容器、mcp 无 Redis、RPC 转发写法、模板按应用隔离、部署只发模板,见 §2.1,并据此改了 §3.4/§3.5/§4.2a/§5.4/§5.5/§6.4) |
|||
> 范围:`apps/services`(后端 home/mcp)、`apps/client`(Flutter)、百炼控制台配置 |
|||
> 状态:**设计稿,未动代码**。三条需求的现状全部按当前 `along` 分支逐文件核过,引用带路径与行号。 |
|||
> 前置文档:[记忆中心-设计与开发文档](记忆中心-设计与开发文档.md)(v0.3,代码已落地);本文在它之上演进,与它冲突的地方在 §5.2 明说。 |
|||
|
|||
--- |
|||
|
|||
## 零、一句话 |
|||
|
|||
三条需求不是三个功能,是**一条数据链的三段**: |
|||
|
|||
``` |
|||
翻译会话(同传/面对面/通话/音视频) ──①──▶ 语音纪要(echomeet_record) ──②──▶ 拾忆时间线(memory_item) |
|||
现场录音 / 通话录音 / 支架 TF 卡 ──已通──▶ │ │ |
|||
│ 待办抽取(已通) │ |
|||
▼ ▼ |
|||
③ EMAI 综合入口:MCP 查(纪要/事项/统计/报告) + 工具写(待办/闹钟/花销) + 端侧指令(启动功能/设备控制) |
|||
▲ |
|||
耳机唤醒 / 支架 / 车载香薰 / App 对话页 —— 同一个会话持有者、同一套 MCP 身份 |
|||
``` |
|||
|
|||
- ① 翻译转写**已经在客户端手里**(`TranslationItem` 列表),差的只是把它送进纪要并「只总结、不转写」。服务端这条通道**已经存在但从没被调用过**(§3.1)。 |
|||
- ② 纪要目前只把「待办」抽进拾忆,**会议本身不在时间线上**;翻译会话更是连服务端都没到过。做法是给拾忆加一个 `meeting` 分类的**索引项**,正文不复制(§4)。 |
|||
- ③ EMAI 已有 MCP 读工具与 JWT 身份链路,缺的是**写工具**、指令清单、与多设备/多 agent 的统一口径(§5)。最大的一处改口径:**记忆类写操作从「端侧指令」改走「MCP 服务端工具」**,理由见 §5.2。 |
|||
|
|||
--- |
|||
|
|||
## 一、现状盘点(三段的地基) |
|||
|
|||
### 1.1 段 ①:翻译会话在客户端的形态 |
|||
|
|||
| 项 | 现状 | 位置 | |
|||
|---|---|---| |
|||
| 模式 | 四个裸字符串 `simultaneous` / `faceToFace` / `audioVideo` / `call`,无枚举 | [translation_controller.dart:160](../apps/client/lib/modules/translation/controllers/translation_controller.dart#L160) | |
|||
| 转写条目模型 | `TranslationItem{sourceText, translatedText, sourceLanguageCode, targetLanguageCode, timestamp, sessionId, isFirstInSession, isIntermediate, utteranceId, serviceId('A'/'B'), …}` | [translation_models.dart:6](../apps/client/lib/modules/translation/models/translation_models.dart#L6) | |
|||
| 持久化 | **GetStorage**,键 `translation_history_<mode>`,每模式一份,上限 1000 条,无 TTL,**无服务端副本** | [translation_history_controller.dart:14-24](../apps/client/lib/modules/translation/controllers/translation_history_controller.dart#L14) | |
|||
| 会话概念 | 只有 `sessionId`(毫秒时间戳串)+ `isFirstInSession`;没有会话对象、没有起止时间 | [translation_controller.dart:1419](../apps/client/lib/modules/translation/controllers/translation_controller.dart#L1419) | |
|||
| 说话人 | 通话模式 `serviceId` A=己方 / B=对方(**不落盘**);面对面 `activeSpeaker` 1/2 只存在于控制器内存;同传单说话人 | `:1754`、`:2215` | |
|||
| 音频 | 只有 `call` / `simultaneous` 在用户点了录音键时录 WAV 到 `TranslatAudio/`(16k 双声道,左=本地麦,右=对端 TTS);停止时 `RecordingArchive.archive()` **只把音频**送进纪要 | [translation_controller.dart:1150-1192](../apps/client/lib/modules/translation/controllers/translation_controller.dart#L1150),`_shouldArchiveRecording` 明确排除 faceToFace/audioVideo | |
|||
| 转写 ↔ 音频 | **零关联**:`sessionId` 与 WAV 文件名是两个独立时间戳,条目上没有音频偏移 | — | |
|||
| 已有出口 | 历史列表只有「复制 / 删除」;详情页有 `Share.share` 纯文本;没有「生成纪要」 | [translation_long_press_menu.dart:68](../apps/client/lib/modules/translation/views/dialogs/translation_long_press_menu.dart#L68)、[translation_detail_view.dart:745](../apps/client/lib/modules/translation/views/translation_detail_view.dart#L745) | |
|||
| 服务端 | `user_translate` 无状态不落库;`user_usages` 只报秒数;**没有任何翻译文本表** | [api_translate.go:25](../apps/services/modules/user/api_translate.go#L25)、[api_usages.go:52](../apps/services/modules/user/api_usages.go#L52) | |
|||
|
|||
### 1.2 段 ②:语音纪要流水线(服务端 echomeet) |
|||
|
|||
| 项 | 现状 | 位置 | |
|||
|---|---|---| |
|||
| 记录表 | `DBEchoMeetRecord`:`original` / `translate` 两列都是 **`[]ContextStruct{Meetingid, Content, Starttime, Endtime, Speaker}` 的 JSON 串**,`overview` 短总结,`summary` 模板纪要,`asr/mt/llm_svc_id` 三路选路结果 | [echomeet_db.proto:38-78](../apps/proto/echomeet/echomeet_db.proto#L38) | |
|||
| 状态机 | `Unknow(0)→AwaitTranscribing(1)→Transcribing(2)→AwaitSummarizing(3)→Summarizing(4)→Completed(5)→Readed(6)`;失败 `10001/10002` **数值大于 Completed** | 同上 `:7-18` | |
|||
| 建记录 | `echomeet_addrecord`:**`original` 字段已存在**(`repeated ContextStruct`),传了就写 `Original` 并镜像到 `Translate`,状态留 `Unknow` | [api_addrecord.go:49-59](../apps/services/modules/echomeet/api_addrecord.go#L49) | |
|||
| 起任务 | `echomeet_starttask`:VIP 闸门 → 按 `Seconds` 计算力 → 解析模板 → 选路;**`rtype=="VOICETRANSLAT"` 分支跳过 ASR**,直接 `TranslateProcess` + `SubmitAITask`,且不要求 `audiourl` | [api_starttask.go:135-152](../apps/services/modules/echomeet/api_starttask.go#L135) | |
|||
| 总结输入 | `AIProcess` 取 `translate`(空则 `original`)拼成 `[speaker]:content` 行,加 `<remark>`,两路并发出 `summary`/`overview` | [tasks.go:481-622](../apps/services/modules/echomeet/tasks.go#L481) | |
|||
| 总结后 | 起 goroutine 调 `IMemory.ExtractMeetingTodos`(待办抽取,第三路) | [tasks.go:621-644](../apps/services/modules/echomeet/tasks.go#L621) | |
|||
| 默认模板 | **服务端没有「默认模板」概念**;客户端取 GetStorage `meeting_last_template_Data`,没有就列表第 0 项 | [generate_bottom_sheet.dart:70-77](../apps/client/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart#L70) | |
|||
| 会议时间 | 服务端 `Creationtime = time.Now()`,**忽略客户端上报**;客户端 `recording_time.dart` 已按「服务端将来接受」的形状送了 | [api_addrecord.go:31](../apps/services/modules/echomeet/api_addrecord.go#L31) | |
|||
| 客户端归档入口 | `RecordingArchive.archive({path, seconds, tag, notify, createdAt})`:**只收音频**,标题=文件名,没有转写、没有场景参数 | [recording_archive.dart:38](../apps/client/lib/data/services/recording_archive.dart#L38) | |
|||
|
|||
**结论:「已转写 → 只总结」的服务端通道已经存在,客户端一次都没用过**(全仓 `VOICETRANSLAT` 只出现在 `api_starttask.go:135` 一处,没有任何写入方)。 |
|||
|
|||
### 1.3 段 ②→③:纪要有没有「给到拾忆」 |
|||
|
|||
只给了一半: |
|||
|
|||
| 内容 | 是否进拾忆 | 方式 | |
|||
|---|---|---| |
|||
| 纪要里的待办 | ✅ | `meeting_extract.go` 抽成 `category=todo, source=meeting, source_id=记录id` | |
|||
| 会议本身(时间、标题、时长、概览) | ❌ | 不在 `memory_item`,拾忆时间线上看不到「今天开了什么会」 | |
|||
| 翻译会话 | ❌ | 连服务端都没到 | |
|||
| 现场录音 / 通话录音 / 支架 TF 卡 | 进纪要 ✅,进拾忆 ❌ | 同会议本身 | |
|||
|
|||
`memory_item` 的分类白名单是 `alarm/todo/idea/expense`([comm/memory.go:23-28](../apps/services/comm/memory.go#L23)),`source` 已有 `meeting`。 |
|||
|
|||
### 1.4 段 ③:EMAI 现状 |
|||
|
|||
| 项 | 现状 | 位置 | |
|||
|---|---|---| |
|||
| 链路 | 客户端 **直连**百炼 `multimodal-dialog` WebSocket,服务端不在对话链路上 | [bailian_multimodal_service.dart:113](../apps/client/lib/data/services/bailian_multimodal_service.dart#L113) | |
|||
| 身份 | `client_info.user_id = uid`(游客回退 `eaimar-guest`);MCP 身份靠 `user_getmcptoken` 签的 2h 令牌,**按工具名嵌套**塞进 `biz_params.user_defined_params` | `:277-301`、[api_getmcptoken.go](../apps/services/modules/user/api_getmcptoken.go) | |
|||
| 需要令牌的工具清单 | 客户端**写死** `_mcpAuthTools`(7 个),服务端加工具必须同步改 | `:583-591` | |
|||
| 跨会话记忆 | `dialog_id` 由 Started 事件给、只在本连接内回传,**不持久化**;每次重连是新对话。聊天记录本地 200 条 | `:399,474`、[emai_chat_store.dart](../apps/client/lib/data/services/emai_chat_store.dart) | |
|||
| 端侧指令 | 只有 `SET_clock` 落地(双写本地 + `memory_add`);`SEND_message`/`MAKE_A_PHONE_CALL` 在 `_declined`;其余 `unsupported`。**模块注册表的 `directives` 机制接好了但没有任何模块声明** | [assistant_directive_service.dart:62-134](../apps/client/lib/data/services/assistant_directive_service.dart#L62)、[module_registry.dart:88](../apps/client/lib/modules/module_registry.dart#L88) | |
|||
| MCP 读工具(已启用) | GLOBAL:`get_memory_items` / `get_memory_stats` / `get_memory_report` / `search_meeting_notes` / `hefeng_weather` / `get_user_tasks` / `cancel_user_task`;CHINA/OVERSEAS:地图 + 联网搜索 | [modules/mcp/](../apps/services/modules/mcp/) | |
|||
| MCP 鉴权 | 已落地:header JWT 或 `auth_token`(aud=mcp)解 uid,参数 uid 不一致直接拒,无密钥则全拒 | [auth.go:92-162](../apps/services/modules/mcp/auth.go#L92) | |
|||
| MCP 写工具 | **一个都没有**(`add_user_task` 代码里根本没 `AddTool`,是死的) | [tool_allhelp_task.go:31-35](../apps/services/modules/mcp/tool_allhelp_task.go#L31) | |
|||
| 知识库 | **没有**。纪要检索是自家 MySQL ngram 全文(词面匹配),换向量方案的触发条件写在工具头注释里 | [tool_meeting.go:21-53](../apps/services/modules/mcp/tool_meeting.go#L21) | |
|||
| 多 agent | EMAI 与 Smartcar 车载助手是**两个百炼应用**(`bailianAppId` 在 data 层 `AgentModule`),设备唤醒按厂商品类选;共用 `EmaiController`/`BailianMultimodalService`/`AssistantDirectiveService` | [agent_module.dart:52](../apps/client/lib/data/models/agent_module.dart#L52)、[device_ai_session_service.dart:52-72](../apps/client/lib/data/services/device_ai_session_service.dart#L52) | |
|||
| 后台 | `agent_config` 表两台机都是 0 行,页面 9-12 已删;**百炼 app_id/工具绑定当前不受后台管**,在百炼控制台 + 客户端常量里 | [menus.ts:68-71](../apps/admin/app/utils/menus.ts#L68) | |
|||
|
|||
#### ⚠️ MCP 工具注册的四处名字对不上(顺手要修,否则新工具也会同样静默失效) |
|||
|
|||
`AddTool` 只登记 `Groups` 里点了名的工具([module.go:163-171](../apps/services/modules/mcp/module.go#L163)),名字对不上就**不报错、直接不暴露**: |
|||
|
|||
| YAML 里写的 | 代码里的真名 | 后果 | |
|||
|---|---|---| |
|||
| `add_user_task` | 代码 `Start()` 没调 `AddTool` | 永远不暴露;客户端 `_mcpAuthTools` 里还列着它 | |
|||
| `gaode_map_route_navigation` / `google_map_route_navigation` | `maps_route_navigation` / `maps_google_route_navigation` | 导航工具从未启用 | |
|||
| `music_search` / `music_play` | `music_search_play`(CHINA)/ `migu_music_search`(CHINA)/ `spotify_music_play`(未装配) | 音乐工具从未启用 | |
|||
| (缺)`gaode_map_weather` / `finance_query` | 代码归 CHINA 组 | 从未启用 | |
|||
|
|||
([deploy/app/confs/mcp.yaml.example:50-73](../../deploy/app/confs/mcp.yaml.example#L50)) |
|||
|
|||
--- |
|||
|
|||
## 二、架构约束(决定实现方式的硬事实)与设计原则 |
|||
|
|||
### 2.1 现有架构里与本文直接相关的 9 条 |
|||
|
|||
| # | 事实 | 对本设计的约束 | |
|||
|---|---|---| |
|||
| A1 | gateway / home / api / mcp / timer **五个进程打在一个镜像、一个容器**,entrypoint 见任一 pid 退出就杀全部([entrypoint.voitrans.sh:46-48](../apps/services/entrypoint.voitrans.sh#L46)) | 新增模块方法的 `Init` **不能返回 error**;存量回填放 `Start` 里起 goroutine;任何建表/回填失败只记日志 | |
|||
| A2 | echomeet 与 memory **同在 home 进程**,互相通过 `comm.IEchomeet` / `comm.IMemory` 接口调用,不 import 对方([comm/module.go:143](../apps/services/comm/module.go#L143)) | 需求 ② 的四个钩子是进程内接口调用,**不走 RPC、不走队列** | |
|||
| A3 | mcp 是**独立进程**(rpcx 集群服务,端口 7300 HTTP / 7004 RPCX),**只配了 MySQL,没有 Redis、没有 Postgres**([mcp.yaml.example](../../deploy/app/confs/mcp.yaml.example)) | MCP 侧不能做基于 Redis 的幂等/限流;**写操作不在 mcp 进程里实现** | |
|||
| A4 | gateway 转发业务请求的方式是 `RpcCall(ctx, "<服务名>", Rpc_GatewayHttpRoute, {MsgName, Meta{userid,ip}, Message})`,home 侧 `SCompHttpRoute.Rpc_GatewayHttpRoute` 按 `MsgName` 反射分发、从 `Meta.userid` 建会话、**过一遍拦截器**([wservice_comp.go:231](../apps/services/modules/gateway/wservice_comp.go#L231)、[comp_httproute.go:103-135](../apps/services/services/comp_httproute.go#L103)) | mcp 同为 rpcx 服务,**可以用完全相同的调用当「内部网关」**调 home 的 `memory_add/update/complete/del`——写逻辑零复制(§5.4) | |
|||
| A5 | 服务间靠 ETCD 发现,`CLUSTER_TAG` 按应用隔离;每个应用一套容器、一套业务 MySQL、一个 Redis 前缀、一个对外 MCP 域名(测试 `mcp-dev.ymaikj.com`、正式 `mcp.ymaikj.com`,正式反代 9-12 已建) | 百炼应用 ↔ 部署一一对应;`MCP_ADDR` 已是外网域名,三期不需要新的网络工作 | |
|||
| A6 | `dev-deploy.sh` / `prod-deploy.sh` **只下发 `*.example` 模板**,服务器上的真实 `confs/*.yaml` 与 `.env` 要手工改([deploy/app/README.md](../../deploy/app/README.md)) | `mcp.yaml` 的 `Groups` 加工具名、`home.yaml` 的 memory 段,**两台机各改一次并重启容器**;本文不新增 `.env` 变量(复用 `MCP_TOKEN_KEY`/`GATEWAY_TOKEN_KEY`) | |
|||
| A7 | 算力系数存**业务库 `config` 表**,`comm.LoadComputeRates` 30s 缓存;console 「会员与算力」页用**应用连接**读写([api_compute.go:29-73](../apps/services/modules/console/api_compute.go#L29),注释明写 console 不能用包级 mysql) | 新系数 `COMPUTE_RATE_SUMMARY` 三处同改:`comm/compute.go` 常量与 `ComputeRates`、`api_compute.go` 的 key 列表、`compute.vue` 一行 | |
|||
| A8 | 会议公共模板在 **Postgres 公共库、按 `app_name` 隔离**([echomeet/model.go:101-121](../apps/services/modules/echomeet/model.go#L101)),新应用要先在后台「从其它应用复制」 | 默认模板解析必须带 `comm.AppName()`;查不到时自动链路**只建记录不起任务**并记一条日志,不能让翻译结束流程报错 | |
|||
| A9 | 客户端登录时 `Synchrodata` 用 `echomeet_getallrecords` **拉全量整行**(含 `original/translate/summary`),服务端有 `getrecordbriefforuid` 但没接线([model.go:232](../apps/services/modules/echomeet/model.go#L232)) | 翻译会话自动进纪要会让记录数上一个量级;二期要把 `getallrecords` 换成 brief + 按需拉详情(§4.5) | |
|||
|
|||
### 2.2 设计原则(三段共用) |
|||
|
|||
1. **不新造第二套「记录」**。翻译会话进纪要就是一条 `echomeet_record`,进拾忆就是一条 `memory_item`;不建 `translation_session` 表、不建 `timeline` 表。 |
|||
2. **服务端为真相源,客户端本地是缓存**。翻译历史现在只活在 GetStorage,重装即丢;进纪要之后自然有了服务端副本,这是副产品不是目标。 |
|||
3. **拾忆存索引不存正文**。`memory_item.category=meeting` 只放标题/时间/时长/概览摘要 + 指回记录的 `source_id`,正文永远在 `echomeet_record`。两处正文必然漂移。 |
|||
4. **能让模型看到结果的写操作走 MCP,看不到结果的设备动作走端侧指令**(§5.2)。 |
|||
5. **失败隔离不变**:任何附赠链路(进拾忆、抽待办、入索引)失败只记日志,不改主记录状态。 |
|||
6. **凡是「名字对不上就静默失效」的地方补一道启动自检**(MCP 工具组、令牌工具清单)。 |
|||
|
|||
--- |
|||
|
|||
## 三、需求 ①:翻译会话 → 语音纪要(只总结) |
|||
|
|||
### 3.1 复用隐藏通道,把它做成正式契约 |
|||
|
|||
现有两步已经能跑: |
|||
|
|||
``` |
|||
echomeet_addrecord {rtype:"VOICETRANSLAT", original:[ContextStruct…], seconds, title} |
|||
→ 记录落库,Original 写入、Translate 镜像,state=Unknow |
|||
echomeet_starttask {id, formlanguage, tolanguage, tid|templateid} |
|||
→ VOICETRANSLAT 分支:跳过 ASR → TranslateProcess(同语种直接跳)→ SubmitAITask → summary/overview |
|||
``` |
|||
|
|||
**不新增接口族**,只把这条路补齐四处缺口(每处都是现在会踩的坑): |
|||
|
|||
| # | 缺口 | 改法 | |
|||
|---|---|---| |
|||
| a | `addrecord` 只收 `original`;翻译会话**两种语言的文本都现成**,再让服务端 MT 一遍是白花钱 | `EchomeetAddRecordReq` 加 `translate repeated ContextStruct`。传了就写 `Translate`,不再镜像 `Original` | |
|||
| b | `TranslateProcess` 的同语种短路 `rec.Translate = rec.Original`([tasks.go:420-424](../apps/services/modules/echomeet/tasks.go#L420))会把 a 传上来的 `Translate` **抹掉** | VOICETRANSLAT 分支改成:`Translate` 非空且 ≠ `Original` → **跳过** `TranslateProcess`;否则按原逻辑 | |
|||
| c | 没有幂等键。客户端会话结束时网络抖动重试就插两条 | `EchomeetAddRecordReq` 加 `client_key`(= 翻译 `sessionId`),`DBEchoMeetRecord` 加列 + `(uid, client_key)` 唯一索引;命中返回已存在记录而非报错。**空串要现生成**(同 `memory_item.client_key` 那条教训) | |
|||
| d | `Creationtime` 服务端写死 `time.Now()`;`rtype` 只有 4 个值,列表里分不清「同传」还是「通话」 | 加 `meet_time int64` + `tz string`(客户端上报,服务端只校验 2020 < t ≤ now+1d,否则退 now)+ `scene string`(`simultaneous/faceToFace/call/audioVideo/fieldRecording/callRecording/holder/import`)。`rtype` 语义收窄为「服务端怎么处理」,`scene` 负责「从哪来」 | |
|||
|
|||
`EchomeetStartTaskReq` **不改**。默认模板见 §3.6。 |
|||
|
|||
### 3.2 客户端:会话结束 → 组转写 → 建记录 → 触发总结 |
|||
|
|||
挂点:`stopRecognition()`([translation_controller.dart:2004](../apps/client/lib/modules/translation/controllers/translation_controller.dart#L2004))与 `_stopFaceToFaceRecognition()`(`:587`)——这两处是 `currentSessionId` 置空的地方,**四种模式**都经过,不再区分 `_shouldArchiveRecording`。 |
|||
|
|||
``` |
|||
会话结束 |
|||
├─ 取本会话条目:_historyManager 里 sessionId == currentSessionId 且 !isIntermediate |
|||
├─ 门槛:条目 ≥ 3 且 有效时长 ≥ 30s,否则不建记录(避免 10 秒试麦也进列表) |
|||
├─ 组 original / translate(规则见 3.3) |
|||
├─ 新的 TranscriptArchive.archive({scene, sessionId, items, audioPath?, seconds, meetTime, tz}) |
|||
│ ├─ echomeet_addrecord {rtype:VOICETRANSLAT, scene, client_key:sessionId, original, translate, seconds, title, meet_time, tz} |
|||
│ ├─ SqfliteApi.insertMeeting + meetingspeaker(本地立刻可见,不等 Synchrodata) |
|||
│ ├─ 有 WAV → MeetingUploadService.addUpload(id, path, 'TranslatAudio')(scene 白名单里已有) |
|||
│ └─ 「翻译结束后自动生成纪要」开关开 → MeetingTaskService.submitTask(id, formlanguage, tolanguage, isSpeaker, templateid:0, tid:'') |
|||
└─ 提示条「已存入语音纪要」,点击进详情页(复用 RecordingArchive 的 notify 样式) |
|||
``` |
|||
|
|||
- `TranscriptArchive` 与现有 `RecordingArchive` 并列放在 `lib/data/services/`,**不改 `RecordingArchive` 的签名**——它有 4 个音频调用方,加转写参数会把它变成两个函数揉一个。 |
|||
- 有 WAV 时 `audiourl` 由上传服务事后 `uprecord` 补上,与现有链路一致;**总结不等音频**(VOICETRANSLAT 分支不查 `audiourl`)。 |
|||
- `MeetingTaskService.addTask` 现有逻辑「`audiourl` 为空就先挂起、等上传完再提交」([meeting_task_service.dart:57-100](../apps/client/lib/data/services/meeting/meeting_task_service.dart#L57))对这条链路是**反的**:转写记录不需要音频。要么绕过 `addTask` 直接 `submitTask`,要么给 `addTask` 加 `requiresAudio:false`。选后者,一个布尔参数,默认 true 不影响老调用方。 |
|||
|
|||
### 3.3 说话人 / 语言 / 时间戳的归一规则 |
|||
|
|||
| 字段 | 规则 | |
|||
|---|---| |
|||
| `Speaker` | 通话:`A`→`Speaker_1`(本人)、`B`→`Speaker_2`(对方);面对面:`activeSpeaker` 1/2 → `Speaker_1/2`;同传/音视频:全部 `Speaker_1`。**统一下划线**(服务端异步转写路径就是 `Speaker_`+id,[tasks.go:675](../apps/services/modules/echomeet/tasks.go#L675);同步路径的 `Speaker %s` 带空格是既有不一致,顺手统一成下划线) | |
|||
| `original[i].Content` | `sourceText`(允许逐条语种不同——面对面就是交替的) | |
|||
| `translate[i].Content` | 归一到**一种语言** `tolanguage`(= 用户在翻译页设定的目标语,通话/面对面取「本人语种」):`sourceLanguageCode == tolanguage` 用 `sourceText`,`targetLanguageCode == tolanguage` 用 `translatedText`,两者都不是(理论上不会)退 `translatedText` | |
|||
| `Starttime/Endtime` | 毫秒偏移。有 WAV:相对录音开始时刻(需在 `startRecording` 时记下 `_recordingStartedAt`);无 WAV:相对首条 `timestamp`。`Endtime` = 下一条的 `Starttime`,末条 +3000 | |
|||
| `Meetingid` | 服务端回填,客户端填 0 | |
|||
| `formlanguage/tolanguage`(starttask) | 都传 `tolanguage`。这样服务端同语种判断为真,且 b 处改法保证不清空 `Translate` | |
|||
| `title` | `<模式名> <yyyy-MM-dd HH:mm>`,41 语言用现有模式标题 key;不再用文件名当标题 | |
|||
| `seconds` | 有 WAV 用录音秒数;无 WAV 用 `末条 timestamp − 首条 timestamp`,**再取与 VAD 有效时长(`_recordAsrUsageStats` 那套)的较小值**——计费依据,不能虚高 | |
|||
|
|||
⚠️ `serviceId`、`activeSpeaker` 现在**不落盘**(`toJson` 丢弃),所以这套归一**只能在会话结束当场做**,不能事后从历史里补做。要支持「历史会话补生成纪要」得先把 `speaker` 字段加进 `TranslationItem.toJson`——这是一个小改动,建议一并做,否则「历史→纪要」入口只能全标 `Speaker_1`。 |
|||
|
|||
### 3.4 计费与闸门 |
|||
|
|||
`starttask` 按 `Seconds × ForMeeting` 计算力([api_starttask.go:60-99](../apps/services/modules/echomeet/api_starttask.go#L60))。翻译会话在 `user_usages` 里**已经按秒计过一次翻译算力**,再按会议全价算一次是重复计费,且这次没有 ASR 成本。 |
|||
|
|||
**决策:新增用量类型 `ComputeUsageSummary`,系数键 `COMPUTE_RATE_SUMMARY`(算力/分钟),后台「会员与算力」页多一行**。`starttask` 在 `rtype==VOICETRANSLAT` 时用它,其余不变。默认值建议 = 会议系数的 1/2,产品可调。 |
|||
|
|||
落点按 A7:`comm/compute.go` 加常量 + `ComputeRates.Summary` + `ForSummary()`(与 `ForMeeting` 同形:向上取整到分钟、最低 1);`console/api_compute.go` 的 key 列表与 `computeRatesView` 各加一项;`admin/app/pages/compute.vue` 加一行。`config` 表没这一行时**回默认值**(与其它系数同口径,计费不能因配置表缺行而中断)。 |
|||
|
|||
- 流水与埋点**不加新维度**:`DBUserUseLog.Addmeetsecond` 照记(它就是「纪要秒数」台账),`analyze` 仍报 `StatEventMeeting`——后台看板的「会议」列语义变成「纪要生成」,比改 `writeEvent` 的固定 switch + console 看板列划算。写进看板说明即可。 |
|||
- VIP 闸门(`VipRequired 5302`)**照旧**:非 VIP 只得到「记录 + 转写」,不生成总结;详情页的「生成纪要」按钮照常引导续费。 |
|||
- ⚠️ 顺带发现:`echomeet_summary`(重新总结)**完全不过 VIP 和算力闸门**([api_summary.go](../apps/services/modules/echomeet/api_summary.go) 里没有 `ResolveUserVip`/`ApplyComputeUsage`)。这是既有漏洞,本次不修但要记:自动化之后「重新总结」的调用量会上来。 |
|||
|
|||
### 3.5 默认模板 |
|||
|
|||
服务端没有默认模板概念(§1.2)。自动链路没有 UI 让用户挑,需要一个确定的兜底: |
|||
|
|||
**`starttask` / `summary` 在 `templateid==0 && tid==""` 时,服务端按 `tolanguage` 解析默认模板**:优先本应用(`app_name = comm.AppName()`,A8)`echomeet_template` 里 `source='public' && language 匹配 && sort 最大` 的那条(= 客户端列表的第 0 项,两边口径一致),语言退化沿用 `model_cache.go` 的 base-lang 规则。客户端自动链路 `templateid:0, tid:''`;手动链路照旧传用户挑的。 |
|||
|
|||
- 解析走 `cache` 组件的内存快照(模板缓存本就存在,NATS `Rpc_ModifyEchomeetTemplate` 热更),不加查库。 |
|||
- **本应用一条公共模板都没有时**(新部署没复制模板):`starttask` 返回 `TemplateNotFound`,客户端自动链路把它当成「记录已建、总结未起」静默处理并记日志,详情页「生成纪要」照常可手动重试。翻译结束流程**不能因此弹错**。 |
|||
- 以后要在后台加「默认模板」勾选,只改这个解析函数,客户端不用动。 |
|||
|
|||
### 3.6 客户端展示与老包兼容 |
|||
|
|||
- 详情页「转写」Tab 现在 `tasktype >= 3` 才解锁([meeting_details_view.dart:354-393](../apps/client/lib/modules/meeting/views/meeting_details_view.dart#L354)),转写记录建好时 state 还是 `Unknow(0)`,转写看不到。改成 `tasktype >= 3 || textList.isNotEmpty`。 |
|||
- 列表按 `scene` 显示来源图标(同传/面对面/通话/音视频/录音/支架/导入),`rtype` 不再参与展示。 |
|||
- 无音频的记录:播放器区域隐藏,不要放一个点了报错的播放条。`Synchrodata` 已能处理 `audiourl` 空串。 |
|||
- **老包**:多出来的 `scene/meet_time/tz/client_key` 字段被手写 `fromJson` 忽略,无害;老包看到新记录 `type=VOICETRANSLAT`,上传桶选择 `type == 'LOCAL' ? … : 'ExternalAudio'` 落到 ExternalAudio,但老包不会给这类记录上传音频,无影响。 |
|||
- 翻译历史页保留,但每个会话卡片加「查看纪要」(本地 sqflite 里按 `client_key` 反查记录 id)。历史列表长按菜单加「生成纪要」(对 3.3 的 ⚠️ 有依赖)。 |
|||
|
|||
### 3.7 设置项 |
|||
|
|||
「翻译结束后自动生成纪要」:默认**开**;关闭时只建记录不起任务。存 GetStorage,不需要服务端。 |
|||
|
|||
--- |
|||
|
|||
## 四、需求 ②:纪要 → 拾忆时间线 |
|||
|
|||
### 4.1 设计:`category=meeting` 的索引项 |
|||
|
|||
在 `memory_item` 加分类 `meeting`([comm/memory.go](../apps/services/comm/memory.go) 白名单加一行,不改表): |
|||
|
|||
| 字段 | 取值 | |
|||
|---|---| |
|||
| `category` | `meeting` | |
|||
| `source` / `source_id` | `meeting` / 记录 id(与待办抽取同一对键,拾忆里「来自同一场会」能关联) | |
|||
| `client_key` | `meeting:<记录id>`(服务端生成,天然幂等) | |
|||
| `title` | 记录标题 | |
|||
| `detail` | `overview`(短总结,≤ 500 字截断;`summary` 全文**不放**) | |
|||
| `happen_date` / `happen_time` / `tz` | 由 `meet_time` + `tz` 折算(§3.1 d;没有 `meet_time` 的存量记录用 `creationtime` + `Asia/Shanghai`,与待办抽取现状同口径) | |
|||
| `state` | 0;**不参与「完成」语义**,客户端不显示勾选框 | |
|||
| `remind_ahead` | 0(不提醒) | |
|||
| `extra` | `{"scene":"call","seconds":1830,"rec_state":5,"has_audio":true}` | |
|||
| `user_edited` | 恒 false;这类项**只读**(编辑入口跳会议详情页) | |
|||
|
|||
**为什么不让拾忆页直接查 `echomeet_record`**:`memory_list` / `memory_today` / `memory_stats` / 周报月报 / `get_memory_items` 全部只读一张表;再加一个数据源意味着日期过滤、时区、分页、MCP 工具各写两份。索引项一行 200 字节,代价可忽略。 |
|||
|
|||
### 4.2 同步钩子(都在 home 进程内,走 `comm.IMemory`,不跨服务) |
|||
|
|||
`IMemory` 加两个方法([comm/module.go:143](../apps/services/comm/module.go#L143)): |
|||
|
|||
```go |
|||
// UpsertMeetingIndex 会议索引项写入/更新;幂等键 meeting:<recordID>。失败只记日志。 |
|||
UpsertMeetingIndex(ctx, uid, recordID, title, overview, meetTime int64, tz, scene string, seconds int64, recState int32) |
|||
// RemoveBySource 删除来源为 source_id 的索引项(不删待办:那些已经是用户自己的任务)。 |
|||
RemoveBySource(ctx, uid, source, sourceID string) |
|||
``` |
|||
|
|||
| 时机 | 位置 | 动作 | |
|||
|---|---|---| |
|||
| 建记录 | `api_addrecord.go` 末尾 | Upsert(此时 overview 空、rec_state=0) | |
|||
| 总结完成 | `tasks.go AIProcess` 置 `Completed` 之后,与 `extractMemoryTodos` 并排 | Upsert(带 overview、rec_state=5) | |
|||
| 改标题 | `api_modifyrecord.go` | Upsert 标题 | |
|||
| 删记录 | `api_delrecords.go` | `RemoveBySource`;**抽出的待办保留**,只把它们的 `source_id` 留着(拾忆里显示「来源会议已删除」) | |
|||
| 转写/总结失败 | `tasks.go` 置 `TranscribeFail/SummarizFail` | Upsert `rec_state`,拾忆卡片显示「生成失败」而不是永远「生成中」 | |
|||
|
|||
存量记录:memory 模块 `Start` 里起 goroutine 跑一次幂等回填(照 `migrate_task.go` 的写法,按 uid 分批、每批 200 条、批间 sleep 100ms;`client_key` 唯一索引保证重跑不重插;失败只记日志——A1,绝不能让 home 起不来)。回填只读 `echomeet_record` 的 id/title/overview/creationtime/state/seconds 六列,**不要 `SELECT *`**(`original/translate/summary` 三列每行可达几十 KB)。 |
|||
|
|||
### 4.2a 性能与列表接口(A9) |
|||
|
|||
翻译会话自动进纪要后记录数会翻几倍。`Synchrodata` 现在每次登录 `echomeet_getallrecords` 拉全量整行(含三个大文本列)。二期一并做: |
|||
|
|||
- `echomeet_getallrecords` 加 `brief:true` 参数 → 走已有但未接线的 `getrecordbriefforuid`(不返回 `original/translate/summary`); |
|||
- 客户端 `Synchrodata` 改成 brief 同步 + 打开详情时 `echomeet_getrecord` 补拉;老包不传 `brief` 行为不变。 |
|||
- 拾忆页读的是 `memory_item` 索引项,本来就不碰大文本列,不受影响。 |
|||
|
|||
### 4.3 客户端拾忆页 |
|||
|
|||
- `CalendarEventCategory` 加 `meeting`;卡片:来源图标 + 标题 + 时长 + 状态 chip(转写中 / 已生成 / 失败)+ overview 前两行;点击 → `Routes.meetingDetails`(记录 id 从 `source_id`)。 |
|||
- 筛选条加「会议」。 |
|||
- **未知分类兜底已在**(记忆中心 §3.3),所以老包收到 `meeting` 分类不会崩,只是按 todo 卡片显示——可接受。 |
|||
- 每日弹窗/播报:`memory_today` 把 `meeting` 项**排除在播报文案之外**(播「今天有一场 30 分钟的通话翻译」没有意义),列表里照常显示。 |
|||
|
|||
### 4.4 报告与 MCP |
|||
|
|||
- `stat_json` 加 `meeting_count` / `meeting_seconds`;报告 prompt 的「依次覆盖」加一句「会议与录音场次」。 |
|||
- `get_memory_items` 的 `category` 枚举加 `meeting`;EMAI 问「我这周开了几次会」直接从 `get_memory_stats` 答;问内容仍走 `search_meeting_notes`。 |
|||
- 新增 `get_meeting_detail(record_id)`:拿到 overview 之后追问「第二点具体说了什么」用,返回 `summary` 全文(截断 4000 字)。 |
|||
|
|||
--- |
|||
|
|||
## 五、需求 ③:EMAI 综合入口 |
|||
|
|||
### 5.1 中枢模型 |
|||
|
|||
``` |
|||
┌───────────────── 客户端(会话持有者)──────────────────┐ |
|||
App 对话页 ─────┤ EmaiController │ |
|||
耳机按键唤醒 ───┤ DeviceAiSessionService ──▶ BailianMultimodalService ─┼──WS──▶ 百炼应用(EMAI / Smartcar) |
|||
支架 / 香薰 ────┤ │ │ RespondingContent │ │ 工具调用(带 auth_token) |
|||
│ │ ▼ │ ▼ |
|||
│ │ AssistantDirectiveService │ 我们的 MCP(7300) |
|||
│ │ ├─ 设备/功能类指令 → 端侧执行 │ ├─ 读:纪要/事项/统计/报告/天气/地图/联网 |
|||
│ │ └─ tool_infos → 触发本地刷新 │ └─ 写:待办/闹钟/灵感/花销/完成/取消 |
|||
└────────┴────────────────────────────────────────────────┘ │ 直查业务库(uid 来自令牌) |
|||
▼ |
|||
memory_item / echomeet_record |
|||
``` |
|||
|
|||
四条链路(App、耳机、支架、香薰)**已经**共用同一个会话持有者与指令分发器(§1.4),本节不动这个结构,只补三件事:写路径、指令清单、身份清单下发。 |
|||
|
|||
### 5.2 决策:记忆类写操作从端侧指令改走 MCP 工具 |
|||
|
|||
记忆中心文档 §8.3 定的是「记录走端侧、查询走 MCP」,理由是延迟低、离线能先落本地。**本文推翻这条**,理由: |
|||
|
|||
1. **模型看不到端侧指令的执行结果**。`tool_calls` 下发即结束,模型在同一帧里已经说了「已经帮你定好了」——CLAUDE.md 记录的「取消闹钟 / 记待办不下发指令却回已取消」就是这种结构的必然产物。MCP 工具的返回会进入模型的下一句话,「记好了,明天下午三点」是真的。 |
|||
2. **「离线先落本地」不成立**。指令只在百炼会话建立时才收得到,会话本身就要联网;不存在「离线拿到指令」这回事。 |
|||
3. **耳机在后台唤醒**这个场景两种方式一样:会话都跑在手机进程里。 |
|||
4. 写路径在服务端,幂等、限流、注销清表全在一处;端侧指令那套 `client_key` 双写、`syncPending` 补传、本地 `reminders` 迁移期双写全部可以退役。 |
|||
|
|||
**保留端侧的**:所有「结果在设备上」的动作——启动/停止某个功能、音量、地图跳转、播放控制。这些 MCP 做不了。 |
|||
|
|||
**过渡**:`SET_clock` 指令处理器保留一个版本(百炼控制台摘掉工具前老应用仍会下发),新老同时存在时按 `client_key` 幂等不会重。 |
|||
|
|||
### 5.3 端侧指令清单(保留 / 新增 / 摘除) |
|||
|
|||
| 指令 | 处置 | 落点 | |
|||
|---|---|---| |
|||
| `SET_clock` | 保留一版作过渡,之后由 MCP `add_memory_item(category=alarm)` 取代 | 现有 | |
|||
| `SEND_message` / `MAKE_A_PHONE_CALL_phone_call` | **百炼控制台摘除**(`_declined` 保留兜底) | 外部操作 | |
|||
| `ROUTE_map`(endLoc_city/endLoc_poi) | 实现:拼高德/Google 地图 URL scheme 打开外部应用;国内外按 region | 新增 | |
|||
| `INCREASE_DEFAULT_volume` / `DECREASE_…` | 实现:系统媒体音量 ±(现有 `volume_controller` 类插件或原生 MethodChannel) | 新增 | |
|||
| `START_feature(module_id, params)` | **新增,走模块注册表 `directives`**:`simultaneous/faceToFace/call/audioVideo/fieldRecording/callRecording/meeting` 各自在 `module.dart` 声明,参数含 `from/to` 语种;这是 `AgentModuleDescriptor.directives` 第一次真正被用 | 各模块 | |
|||
| `STOP_feature` | 同上 | 各模块 | |
|||
| `MUSIC_control(play/pause/next)` | 车载香薰已有 `commands` 形状(`intent_info`),归到同一分发器 | smartcar | |
|||
|
|||
`AssistantDirectiveService._dispatch` 的顺序不变:declined → 模块 handler → 内置 → unsupported。`unsupported` 继续记流水,这是发现百炼新下发了什么的唯一渠道。 |
|||
|
|||
⚠️ `START_feature` 的能力校验:模块声明了 `requiredCapabilities`(通话翻译要 `callAudioTap`),指令到达时 `DeviceHub.to.has(cap)` 不满足要把「缺设备」作为结果**告诉用户**(本地 TTS 一句「需要连接耳机」),而不是静默 unsupported——这条指令的用户正对着设备说话。 |
|||
|
|||
### 5.4 MCP 工具清单 |
|||
|
|||
**新增写工具**(GLOBAL,全部要 `auth_token`): |
|||
|
|||
| 工具 | 参数 | 落点 | 返回 | |
|||
|---|---|---|---| |
|||
| `add_memory_item` | `category`(todo/alarm/idea/expense)、`title`、`date`(YYYY-MM-DD,缺省今天)、`time`(HH:mm)、`repeat`、`amount`(元,expense)、`detail` | `memory_item`,`source=assistant` | `{id, title, happen_date, happen_time}` | |
|||
| `complete_memory_item` | `id` 或 `title_keyword` | `state=1` | 完成的那条 | |
|||
| `cancel_memory_item` | 同上 | `state=2` | 取消的那条;**这就是「取消闹钟」的落点** | |
|||
| `update_memory_item` | `id` + 改动字段 | `user_edited=true` | 改后的那条 | |
|||
| `get_meeting_detail` | `record_id` | 只读 | overview + summary(截断) | |
|||
|
|||
**实现方式:mcp 进程当「内部网关」,写逻辑留在 home(A3 + A4)** |
|||
|
|||
``` |
|||
百炼 → mcp:7300 add_memory_item(args, auth_token) |
|||
│ ResolveUIDWithToken → uid(现有 auth.go) |
|||
│ 组 pb.DBMemoryItem(分类/日期/时间/金额归一,见下) |
|||
▼ |
|||
this.module.Service().RpcCall(ctx, "home", comm.Rpc_GatewayHttpRoute, |
|||
&pb.Rpc_GatewayHttpRouteReq{MsgName:"memory_add", |
|||
Meta:{userid: uid, ip: "mcp", servicetag: <本集群>}, |
|||
Message: json(MemoryAddReq)}, &reply) |
|||
▼ |
|||
home SCompHttpRoute.Rpc_GatewayHttpRoute → memory.apiComp.Add(校验、幂等、recalcRemind、落库) |
|||
▼ |
|||
reply.Body(业务 JSON)→ 转成可朗读短句回给模型 |
|||
``` |
|||
|
|||
- 这是 gateway 转发业务请求的**原样写法**([wservice_comp.go:231](../apps/services/modules/gateway/wservice_comp.go#L231)),只是调用方从 gateway 换成 mcp;mcp 本就是 rpcx 集群服务,同 `CLUSTER_TAG` 下 ETCD 能发现 home。**mcp 模块里从没 `RpcCall` 过**,这是第一次,先用 `get_memory_items` 之外的一个只读工具(`get_meeting_detail`)走通再上写工具。 |
|||
- 分类白名单、日期校验、`recalcRemind`、`client_key` 幂等、`user_edited` 语义**全部复用 home 的 `memory_add/update/complete/del`**,mcp 侧零业务逻辑——记忆中心 §8.2 担心的「第二份实现漂移」不存在。 |
|||
- home 侧拦截器(实名/VIP 类)对 RPC 进来的请求同样生效([comp_httproute.go:135](../apps/services/services/comp_httproute.go#L135)),EMAI 已过实名闸门所以不会被拦;万一被拦,返回给模型的是同一句业务提示。 |
|||
- mcp 没有 Redis(A3):写工具的**限流**用 mcp 进程内的按 uid 令牌桶(每应用单进程,够用,重启即清零可接受);**幂等**不需要 Redis——`client_key` 由 mcp 生成:`mcp:<sha1(uid|category|title|happen_date|happen_time)>`,home 的 `(uid, client_key)` 唯一索引天然去重,命中返回 `Duplicated:true`([api_add.go](../apps/services/modules/memory/api_add.go) 已有这个返回)。 |
|||
- RPC 超时 5s;超时/失败回给模型「没记上,请再说一次」,**不要**回「已记好」。 |
|||
|
|||
- **今天是几号由服务端给**:工具入参日期允许省略,mcp 按 `user_getmcptoken` 时上报并签进令牌 claims 的 `tz` 折算「今天」(令牌 2h 内时区不会变;不查库)。不能靠模型自己算日期——它不知道今天几号,`SET_clock` 那次 `03:30` 的坑就是这么来的。 |
|||
- **幂等**:见上,靠 `client_key` 唯一索引;同一句话被模型重试两次只会落一条。 |
|||
- **写工具的 `title_keyword` 匹配命中多条时不动手**,返回候选让模型追问——宁可多说一句,不能删错。 |
|||
- 写工具的错误码走 `pb.ErrorCode` 5201 段(记忆中心已开),返回给模型的是可朗读的中文短句,不是 code。 |
|||
|
|||
**修正现有**:§1.4 表里四处名字对齐;`add_user_task` / `get_user_tasks` / `cancel_user_task` 三个 allhelp 旧工具**下线**(`DBTask` 已迁到 `memory_item`,记忆中心 §9.1),从 YAML 与 `_mcpAuthTools` 一并删。 |
|||
|
|||
**启动自检**:`mcp` 模块 `Start` 结束时,对每个 `AddTool` 过的名字若不在任何组 → `safeLogErrorf` 一行;YAML 里点名但没有实现的 → 同样一行。名字对不上不再是「安静地没有」。 |
|||
|
|||
### 5.5 身份链路:令牌工具清单改由服务端下发 |
|||
|
|||
现在 `_mcpAuthTools` 写死在客户端(§1.4),服务端每加一个要身份的工具就要发版。改: |
|||
|
|||
- `UserGetMcpTokenResp` 加 `auth_tools repeated string`。清单的**唯一来源是 `comm.McpAuthTools`**(一个字符串切片常量):mcp 模块用它判定哪些工具必须解身份,user 模块用它下发。user 在 home 进程、读不到 `mcp.yaml`,而五个服务出自同一个镜像(A1),放 `comm` 就是同一份代码、同一次构建,不会漂移;`comm/mcpauth_test.go` 断言每个 `tool_*.go` 里调了 `ResolveUID*` 的工具名都在这个切片里。 |
|||
- 令牌 claims 加 `tz`(客户端 `user_getmcptoken` 请求带 IANA 时区),供 §5.4 的「今天」折算。 |
|||
- 客户端 `BailianMultimodalService` 按下发清单嵌套 `user_defined_params`,本地清单退为兜底。 |
|||
- 百炼**只透传给被调用的那个工具**,多嵌几个没有成本。 |
|||
|
|||
⚠️ 记忆文件里的实测:**只有按工具名嵌套这一种形状能收到**,平铺 / 按 MCP 服务名嵌套 / `mcp` 子节点三种都静默丢弃。别改形状。 |
|||
|
|||
### 5.6 会话记忆与上下文注入(两条都要先验证) |
|||
|
|||
| 目标 | 方案 | 验证方法 | |
|||
|---|---|---| |
|||
| 跨会话记忆(重开 App 还记得上一轮) | Start 的 `input` 里回传上次的 `dialog_id`(现在只在 continue-task 里回传,Start 不传,[bailian_multimodal_service.dart:399](../apps/client/lib/data/services/bailian_multimodal_service.dart#L399))。持久化到 GetStorage,按 uid 分,超过 24h 作废 | **必须做对照组**:一组传真 dialog_id,一组传乱写的 id,一组传虚构字段名。三组表现一致 = 字段被静默丢弃,方案不成立(这是 `rag_options` 那次实验的教训) | |
|||
| 模型知道「今天几号 / 用户在哪个时区 / 接着哪个设备 / 今天有什么事」 | `biz_params.user_prompt_params`(协议里**确实存在**的字段)注入 `{"user_context": "..."}`,百炼应用提示词里放 `${user_context}` 占位。内容 ≤ 300 字:日期、时区、设备、未来 3 条事项标题 | 控制台提示词里加占位 → 真机问「今天几号」;同样加对照组 | |
|||
|
|||
两条都不成立时的退路:跨会话记忆靠服务端把最近 N 轮对话摘要(`allhelp` 的 `DBChatSummary` 机制已有)作为 `user_context` 的一部分注入——这条只依赖 `user_prompt_params`,不依赖百炼的会话保持。 |
|||
|
|||
### 5.7 联网与知识库 |
|||
|
|||
- **联网**:百炼应用自带 `enable_web_search`(客户端已按开关发 `extra_config`),与我们 MCP 的 `bocha/tavily` 是两条路。**定一条**:国内用百炼内置(省一次工具往返),海外用 `tavily`(内置搜索对海外站点覆盖未验证)。 |
|||
- **知识库**:**不建**。纪要检索继续用 `search_meeting_notes`(MySQL ngram 全文),换向量/百炼知识库的三个触发条件已写在 [tool_meeting.go:42-53](../apps/services/modules/mcp/tool_meeting.go#L42),到了再评估。原因复述一遍免得再议:对话协议里没有入口告诉知识库「只检索这个用户的文件」,隔离只能落在我们自己这一侧;MCP 工具刚好就是这一侧。 |
|||
- 翻译会话进纪要之后自动进入同一个检索范围(`echomeet_record.summary` 有全文索引),不用另做。灵感/待办的关键词检索由 `get_memory_items` 加 `keyword` 参数覆盖(`title/detail` LIKE,量小够用)。 |
|||
|
|||
### 5.8 多设备 / 多 agent 统一 |
|||
|
|||
- EMAI 与 Smartcar 是两个百炼应用。**约束:两个应用绑定同一个 MCP 服务、同一份工具勾选、同一份提示词变量**,否则「耳机能记待办、香薰不能」。做成一份控制台配置清单(§5.9),每次加工具按清单过两遍。 |
|||
- 设备唤醒选哪个应用:现有 `AgentDefaults.categoryForVendor → defaultsForCategory → bailianAppId` 不动。 |
|||
- 游客:`client_info.user_id` 回退 `eaimar-guest` 会让多设备串记忆;MCP 侧无令牌一律拒,所以游客只能闲聊——这是对的。EMAI 已过实名闸门,游客本来进不来。 |
|||
- 车载香薰的 `commands` 形状(`intent_info`)与 `tool_calls` 形状都进 `AssistantDirective.parseFromOutput`,分发器不分设备。 |
|||
|
|||
### 5.9 百炼控制台配置清单(外部操作,每个应用各做一遍) |
|||
|
|||
| # | 操作 | 应用 | |
|||
|---|---|---| |
|||
| C1 | MCP 服务「emai-mcp」:技能 → MCP 服务 → **逐个勾选**新工具(`add/complete/cancel/update_memory_item`、`get_meeting_detail`),点发布 | EMAI、Smartcar | |
|||
| C2 | 摘掉 `SEND_message`、`MAKE_A_PHONE_CALL_phone_call`、`SET_clock`(写工具上线一版之后) | 两者 | |
|||
| C3 | 端侧函数加 `START_feature` / `STOP_feature` / 音量 / `ROUTE_map`(参数按 §5.3) | 两者 | |
|||
| C4 | 提示词加 `${user_context}` 占位(§5.6 验证通过后) | 两者 | |
|||
| C5 | 开 `enable_web_search`(国内应用) | EMAI | |
|||
| C6 | 后台 `svc_config` 的 `sts_bailian.app_id / workspace_id` 填实值,别再靠客户端硬编码兜底 | 后台 | |
|||
|
|||
⚠️ 控制台「语音交互体验」里测不出身份链路(没有客户端就没有 `user_defined_params`),验证只能真机或脚本直连 WS。 |
|||
|
|||
--- |
|||
|
|||
## 六、跨需求的公共改动汇总 |
|||
|
|||
### 6.1 proto |
|||
|
|||
| 文件 | 改动 | |
|||
|---|---| |
|||
| `echomeet_msg.proto` `EchomeetAddRecordReq` | + `translate`(9, repeated ContextStruct) `client_key`(10) `meet_time`(11) `tz`(12) `scene`(13) | |
|||
| `echomeet_db.proto` `DBEchoMeetRecord` | + `scene`(33) `meet_time`(34) `tz`(35) `client_key`(36, `uniqueIndex:idx_echo_uid_ck` 与 uid 联合) | |
|||
| `memory_msg.proto` `MemoryStatsResp` | + `meeting_count` `meeting_seconds` | |
|||
| `memory_msg.proto` `MemoryListReq` | + `keyword` | |
|||
| `user_msg.proto` `UserGetMcpTokenReq/Resp` | Req + `tz`;Resp + `auth_tools` | |
|||
| `errorcode.proto` | 5201 段续编:`MemoryItemAmbiguous`(写工具命中多条)、`EchomeetTranscriptEmpty` | |
|||
|
|||
生成后 `git diff --stat -- apps/services/pb/` 必须只动这几个文件(CLAUDE.md 那条 protoc-gen-go 版本坑)。 |
|||
|
|||
### 6.2 服务端 |
|||
|
|||
| 模块 | 改动 | |
|||
|---|---| |
|||
| `comm` | `MemoryCatMeeting`;`ComputeUsageSummary` + `COMPUTE_RATE_SUMMARY`;`IMemory` 加 `UpsertMeetingIndex/RemoveBySource`;`meet_time` 校验函数 | |
|||
| `echomeet` | addrecord 收新字段 + 幂等;starttask VOICETRANSLAT 分支不清空 Translate、走 Summary 计费、默认模板解析;四个钩子调 IMemory;`Speaker` 前缀统一 | |
|||
| `memory` | 分类白名单;两个新方法;stats 加会议;list 加 keyword;启动回填存量记录索引 | |
|||
| `mcp` | 5 个新工具(写工具经 RPCX 调 home,`client_key` 由 mcp 生成);名字对齐;下线 allhelp 三工具;启动自检;进程内按 uid 限流 | |
|||
| `user` | `user_getmcptoken` 下发 `comm.McpAuthTools`、收 `tz` 并签进 claims | |
|||
| `echomeet` | `getallrecords` 加 `brief` 参数接上 `getrecordbriefforuid` | |
|||
| `console` | 「会员与算力」页加 `COMPUTE_RATE_SUMMARY` 一行(`api_compute.go` + `compute.vue`) | |
|||
| 注销 | `cancel()` 表清单**不用改**(`echomeet_record`、`memory_item` 都在) | |
|||
|
|||
### 6.3 客户端 |
|||
|
|||
| 位置 | 改动 | |
|||
|---|---| |
|||
| `data/services/transcript_archive.dart` | 新:组转写 → addrecord → 本地库 → 上传 → 起任务 | |
|||
| `translation_controller.dart` | 两处结束挂点;`_recordingStartedAt`;`TranslationItem` 加 `speaker` 并落盘 | |
|||
| `meeting_task_service.dart` | `addTask(requiresAudio:)` | |
|||
| `meeting_details_view.dart` | 转写 Tab 解锁条件;无音频隐藏播放器 | |
|||
| 拾忆页 | `meeting` 分类卡片 + 筛选 + 跳转 | |
|||
| `assistant_directive_service.dart` | `tool_infos` 解析 → 命中写工具名则 `MemoryService.refresh()` + `MemoryReminderScheduler.reschedule()`;`ROUTE_map`/音量处理器 | |
|||
| 各 `module.dart` | 声明 `directives: {START_feature…}` | |
|||
| `bailian_multimodal_service.dart` | 令牌清单改读服务端;Start 回传 `dialog_id`(验证通过后);`user_prompt_params` | |
|||
| `mcp_token_service.dart` | 缓存 `auth_tools`,上报 `tz` | |
|||
| 设置页 | 「翻译结束后自动生成纪要」 | |
|||
|
|||
⚠️ `tool_infos` 的精确形状(工具名字段叫什么、结果在哪一层)**要先抓一帧**再写解析,记忆中心那次只确认了「它是服务端已执行的结果」。 |
|||
|
|||
--- |
|||
|
|||
### 6.4 落地清单(按环境;A5/A6 决定了这些都是手工步骤) |
|||
|
|||
服务端改动全部在 `starpivot-app` 一个镜像里,走既有 `dev-deploy.sh along` / `prod-build.sh + prod-deploy.sh ym along`。**代码之外**要动的: |
|||
|
|||
| 步骤 | 测试机 8.133.166.29 `/home/work/starpivot/app` | 正式机 47.116.104.181 `/home/work/ym-a11` | 哪一期 | |
|||
|---|---|---|---| |
|||
| `confs/home.yaml` | `memory` 段无新键(回填与钩子无需配置) | 同 | 二期 | |
|||
| `confs/mcp.yaml` `Groups.GLOBAL` | 加 `add/complete/cancel/update_memory_item`、`get_meeting_detail`;删 `add/get/cancel_user_task`、`music_search`、`music_play`;导航项改成代码真名 | 同 | 三期 | |
|||
| `.env` | **不新增**(复用 `MCP_TOKEN_KEY` / `GATEWAY_TOKEN_KEY`,正式机 9-11 已配) | 同 | — | |
|||
| 业务库 `config` 表 | `COMPUTE_RATE_SUMMARY` 由后台「会员与算力」页写入;不写走默认 | 同 | 一期 | |
|||
| `echomeet_template` | 确认本应用(`app_name=EAIMAR`)有公共模板,否则默认模板解析落空 | 同 | 一期 | |
|||
| 重启 | 改 `confs/*.yaml` 后 `docker compose up -d --force-recreate`(Groups 只在启动时读) | 同 | 三期 | |
|||
| NPM 反代 | `mcp-dev.ymaikj.com` 已有 | `mcp.ymaikj.com` 9-12 已建(SSE 关缓冲) | 无需动 | |
|||
| 百炼控制台 | §5.9 C1~C6,EMAI 与 Smartcar 各一遍 | 同一套应用 | 三期 | |
|||
| 客户端 | 一期/二期改动**不依赖**服务端先上(addrecord 多传的字段老服务端忽略,只是转写不落库);三期客户端依赖 `auth_tools` 下发,**服务端先上** | — | — | |
|||
|
|||
⚠️ 顺序:一期服务端先于客户端上(否则翻译结束建出的记录 `translate` 会被同语种短路抹掉,用户看到的总结是原文语种)。二期无顺序要求。三期服务端 → 百炼控制台 → 客户端。 |
|||
|
|||
## 七、分期实施 |
|||
|
|||
### 一期:翻译 → 纪要(两周内可交付,服务端改动最小) |
|||
|
|||
| # | 任务 | 端 | |
|||
|---|---|---| |
|||
| 1.1 | proto:`AddRecordReq` 5 字段 + `DBEchoMeetRecord` 4 列 | 后端 | |
|||
| 1.2 | addrecord 幂等 + `translate` 直写 + `meet_time/tz/scene`;starttask VOICETRANSLAT 分支修 Translate 清空、默认模板解析 | 后端 | |
|||
| 1.3 | `ComputeUsageSummary` + 后台系数行 | 后端/后台 | |
|||
| 1.4 | `TranslationItem.speaker` 落盘;`TranscriptArchive`;两处结束挂点;`addTask(requiresAudio)`;自动生成开关 | 客户端 | |
|||
| 1.5 | 详情页转写 Tab 解锁、无音频播放器隐藏、列表 scene 图标 | 客户端 | |
|||
| 1.6 | 翻译历史会话卡「查看纪要」 | 客户端 | |
|||
|
|||
**验收**:四种模式各跑一段 ≥ 30s 的会话 → 结束 3s 内语音纪要列表出现记录、转写 Tab 可看、说话人分行正确;VIP 用户自动出总结;非 VIP 只有转写;`user_getcompute` 里多出的算力 = 时长 × Summary 系数;同一会话重复触发不插第二条。 |
|||
|
|||
### 二期:纪要 → 拾忆 |
|||
|
|||
| # | 任务 | 端 | |
|||
|---|---|---| |
|||
| 2.1 | `MemoryCatMeeting`、`IMemory` 两方法、echomeet 四钩子、存量回填 | 后端 | |
|||
| 2.2 | `memory_stats` 会议字段、`memory_today` 播报排除、报告 prompt | 后端 | |
|||
| 2.3 | 拾忆页 meeting 卡片/筛选/跳转 | 客户端 | |
|||
| 2.4 | `get_memory_items` 分类枚举加 meeting;`get_meeting_detail`(**经 RPCX 调 home,作为三期写工具的链路预演**) | 后端 MCP | |
|||
| 2.5 | `echomeet_getallrecords` 加 `brief`;客户端 `Synchrodata` 改 brief + 详情按需拉 | 两端 | |
|||
|
|||
**验收**:一期产生的记录在拾忆当天格子里出现,状态 chip 随转写/总结推进;删记录后索引项消失、抽出的待办还在;周报里有「本周 N 场会议共 M 分钟」。 |
|||
|
|||
### 三期:EMAI 综合入口 |
|||
|
|||
| # | 任务 | 端 | |
|||
|---|---|---| |
|||
| 3.1 | MCP 名字对齐 + 启动自检 + 下线 allhelp 三工具 | 后端 | |
|||
| 3.2 | 四个写工具:mcp 组 item → RPCX 调 home `memory_*`;`client_key` 由 mcp 生成;进程内限流;按令牌 `tz` 折算「今天」 | 后端 | |
|||
| 3.3 | `comm.McpAuthTools` 单一来源 + 守卫测试;`user_getmcptoken` 下发 `auth_tools`、签 `tz`;客户端改读 | 两端 | |
|||
| 3.8 | 两台机 `confs/mcp.yaml` Groups 手改 + 重启(§6.4) | 运维 | |
|||
| 3.4 | `tool_infos` 抓帧 → 解析 → 本地刷新与重排通知 | 客户端 | |
|||
| 3.5 | `START/STOP_feature` 走模块注册表;`ROUTE_map`;音量 | 客户端 | |
|||
| 3.6 | 百炼控制台 C1~C6 | 外部 | |
|||
| 3.7 | `dialog_id` 回传与 `user_prompt_params` 两个对照实验;通过则接入 | 客户端 | |
|||
|
|||
**验收**:对耳机说「明天下午三点提醒我开会」→ 模型回复里带服务端返回的日期时间 → 拾忆页 2s 内出现该项 → 本地通知已排;说「取消它」→ `state=2`;说「开始通话翻译,中译英」→ 翻译页打开且模式/语种正确,未连耳机时 TTS 提示;香薰上同一套说法结果一致。 |
|||
|
|||
--- |
|||
|
|||
## 八、风险与待确认 |
|||
|
|||
### 风险 |
|||
|
|||
| 级别 | 风险 | 应对 | |
|||
|---|---|---| |
|||
| 高 | **`TranslateProcess` 同语种短路会抹掉客户端传的 `Translate`**,表现为总结用的是原文语种、且不报错 | §3.1 b,一期必改;加单测:VOICETRANSLAT + 非空 Translate → AIProcess 输入等于 Translate | |
|||
| 高 | **MCP 工具名对不上就静默不暴露**,新写工具会重蹈 `add_user_task` 覆辙 | §5.4 启动自检;新工具上线用真机验一次 | |
|||
| 高 | **`_mcpAuthTools` 与服务端脱节** → 新工具永远「身份校验失败」 | §5.5 改服务端下发;过渡期两边都列 | |
|||
| 高 | 写工具的模型幻觉方向反转:以前是「没做说做了」,MCP 之后可能「做了两遍」(模型重试) | 120s 幂等窗口 + 结果里 `duplicated` 标记 | |
|||
| 高 | `dialog_id` / `user_prompt_params` 两条百炼能力**未验证**,且失败是静默的 | 对照组实验先于编码(§5.6) | |
|||
| 高 | **mcp → home 的 RPCX 调用是这个模块第一次跨服务调用**,ETCD 发现/`CLUSTER_TAG`/超时任何一环不通都表现为「工具超时」 | 先用只读的 `get_meeting_detail` 走通链路再上写工具;RPC 超时 5s 回「没记上」 | |
|||
| 高 | 新应用没复制公共模板 → 自动总结 `TemplateNotFound` | 客户端静默降级为「只建记录」;上线清单里核对模板(§6.4) | |
|||
| 中 | `getallrecords` 全量整行同步随记录数膨胀 | 二期 `brief` 参数 + 详情按需拉(§4.2a) | |
|||
| 中 | 五进程同容器:memory 回填或新钩子 panic 会拖垮全站 | 回填在 goroutine + recover;钩子沿用 `ExtractMeetingTodos` 的 recover 写法 | |
|||
| 中 | 翻译会话自动进纪要 → 列表被短会话刷屏 | 3 条 / 30s 门槛 + 设置开关 | |
|||
| 中 | 重复计费:翻译秒 + 纪要秒 | 独立 Summary 系数(§3.4) | |
|||
| 中 | `echomeet_summary` 不过闸门,自动化后调用量上升 | 记为既有漏洞,单独排期 | |
|||
| 中 | 面对面/通话的说话人信息现在不落盘,历史会话补生成纪要全是 Speaker_1 | `TranslationItem.speaker` 一期就加 | |
|||
| 中 | 拾忆 `meeting` 项进每日播报会念出无意义内容 | `memory_today` 播报排除该分类 | |
|||
| 中 | 存量记录回填索引时 `meet_time` 空,全部按上传日期 | 已知偏差,与待办抽取一致,写进说明 | |
|||
| 中 | EMAI 与 Smartcar 两个百炼应用工具配置漂移 | 控制台清单 §5.9,加工具按清单过两遍 | |
|||
| 低 | 老包收到 `category=meeting` | 兜底已发(记忆中心一期) | |
|||
|
|||
### 待确认(产品/运营) |
|||
|
|||
1. **翻译会话默认自动生成纪要还是默认只存转写**?默认自动会持续消耗算力与 VIP 用户的耐心(每次通话结束都多一条纪要);默认关则功能等于没做。建议**默认开 + 30s 门槛**,两周后看 `user_getcompute` 的 Summary 用量再定。 |
|||
2. **`COMPUTE_RATE_SUMMARY` 的初值**。建议会议系数的 1/2。 |
|||
3. **记忆写操作改走 MCP**(§5.2)是否接受——它推翻记忆中心文档 §8.3,且意味着 `SET_clock` 那套本地双写在一个版本后退役。 |
|||
4. **`START_feature` 要不要让香薰能拉起手机上的翻译页**:技术上是同一条链,但用户在车里对着香薰说「开始通话翻译」、手机自己跳页,体验是否合理。 |
|||
5. 拾忆里「来源会议已删除」的待办要不要跟着删。建议不删(已经是用户的任务)。 |
|||
6. 联网搜索国内走百炼内置、海外走 tavily——需要确认百炼内置搜索在海外应用上的表现,没验证过。 |
|||
|
|||
--- |
|||
|
|||
## 附录 A:关键文件索引 |
|||
|
|||
| 用途 | 路径 | |
|||
|---|---| |
|||
| 隐藏的只总结通道 | `apps/services/modules/echomeet/api_starttask.go:135`、`api_addrecord.go:49` | |
|||
| 同语种短路(要改) | `apps/services/modules/echomeet/tasks.go:420` | |
|||
| 总结 + 待办抽取挂点 | `apps/services/modules/echomeet/tasks.go:481-644` | |
|||
| 翻译条目模型 / 历史存储 | `apps/client/lib/modules/translation/models/translation_models.dart`、`controllers/translation_history_controller.dart` | |
|||
| 会话结束挂点 | `apps/client/lib/modules/translation/controllers/translation_controller.dart:587,2004` | |
|||
| 音频归档(不改签名) | `apps/client/lib/data/services/recording_archive.dart` | |
|||
| 任务提交 / 轮询 | `apps/client/lib/data/services/meeting/meeting_task_service.dart` | |
|||
| 记忆中心跨模块接口 | `apps/services/comm/module.go:143`、`comm/memory.go` | |
|||
| MCP 鉴权 / 工具组 / 会议工具 / 记忆工具 | `apps/services/modules/mcp/auth.go`、`module.go:163`、`tool_meeting.go`、`tool_memory.go` | |
|||
| MCP 工具组配置 | `deploy/app/confs/mcp.yaml.example:50-73` | |
|||
| MCP 令牌签发 / 客户端缓存 | `apps/services/modules/user/api_getmcptoken.go`、`apps/client/lib/data/services/mcp_token_service.dart` | |
|||
| gateway → home 的 RPC 转发写法(mcp 照抄) | `apps/services/modules/gateway/wservice_comp.go:231`、`apps/services/services/comp_httproute.go:103` | |
|||
| 五进程守护 | `apps/services/entrypoint.voitrans.sh` | |
|||
| 算力系数三处落点 | `apps/services/comm/compute.go:37`、`modules/console/api_compute.go:29`、`apps/admin/app/pages/compute.vue` | |
|||
| 部署方式(只发模板) | `deploy/app/README.md`、`deploy/app/confs/mcp.yaml.example` | |
|||
| 百炼会话 / 令牌嵌套 / dialog_id | `apps/client/lib/data/services/bailian_multimodal_service.dart:277-301,399,474,583` | |
|||
| 指令解析 / 分发 | `apps/client/lib/data/models/assistant_directive.dart`、`data/services/assistant_directive_service.dart` | |
|||
| 模块注册表(directives 未用) | `apps/client/lib/modules/module_registry.dart:88`、`module_descriptor.dart:55` | |
|||
| 设备唤醒选 agent | `apps/client/lib/data/services/device_ai_session_service.dart:52-72` | |
|||
| 算力用量类型 | `apps/services/comm/compute.go:473,543` | |
|||
Loading…
Reference in new issue