59 KiB
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分支逐文件核过,引用带路径与行号。 前置文档:记忆中心-设计与开发文档(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 |
| 转写条目模型 | TranslationItem{sourceText, translatedText, sourceLanguageCode, targetLanguageCode, timestamp, sessionId, isFirstInSession, isIntermediate, utteranceId, serviceId('A'/'B'), …} |
translation_models.dart:6 |
| 持久化 | GetStorage,键 translation_history_<mode>,每模式一份,上限 1000 条,无 TTL,无服务端副本 |
translation_history_controller.dart:14-24 |
| 会话概念 | 只有 sessionId(毫秒时间戳串)+ isFirstInSession;没有会话对象、没有起止时间 |
translation_controller.dart:1419 |
| 说话人 | 通话模式 serviceId A=己方 / B=对方(不落盘);面对面 activeSpeaker 1/2 只存在于控制器内存;同传单说话人 |
:1754、:2215 |
| 音频 | 只有 call / simultaneous 在用户点了录音键时录 WAV 到 TranslatAudio/(16k 双声道,左=本地麦,右=对端 TTS);停止时 RecordingArchive.archive() 只把音频送进纪要 |
translation_controller.dart:1150-1192,_shouldArchiveRecording 明确排除 faceToFace/audioVideo |
| 转写 ↔ 音频 | 零关联:sessionId 与 WAV 文件名是两个独立时间戳,条目上没有音频偏移 |
— |
| 已有出口 | 历史列表只有「复制 / 删除」;详情页有 Share.share 纯文本;没有「生成纪要」 |
translation_long_press_menu.dart:68、translation_detail_view.dart:745 |
| 服务端 | user_translate 无状态不落库;user_usages 只报秒数;没有任何翻译文本表 |
api_translate.go:25、api_usages.go:52 |
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 |
| 状态机 | 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 |
| 起任务 | echomeet_starttask:VIP 闸门 → 按 Seconds 计算力 → 解析模板 → 选路;rtype=="VOICETRANSLAT" 分支跳过 ASR,直接 TranslateProcess + SubmitAITask,且不要求 audiourl |
api_starttask.go:135-152 |
| 总结输入 | AIProcess 取 translate(空则 original)拼成 [speaker]:content 行,加 <remark>,两路并发出 summary/overview |
tasks.go:481-622 |
| 总结后 | 起 goroutine 调 IMemory.ExtractMeetingTodos(待办抽取,第三路) |
tasks.go:621-644 |
| 默认模板 | 服务端没有「默认模板」概念;客户端取 GetStorage meeting_last_template_Data,没有就列表第 0 项 |
generate_bottom_sheet.dart:70-77 |
| 会议时间 | 服务端 Creationtime = time.Now(),忽略客户端上报;客户端 recording_time.dart 已按「服务端将来接受」的形状送了 |
api_addrecord.go:31 |
| 客户端归档入口 | RecordingArchive.archive({path, seconds, tag, notify, createdAt}):只收音频,标题=文件名,没有转写、没有场景参数 |
recording_archive.dart:38 |
结论:「已转写 → 只总结」的服务端通道已经存在,客户端一次都没用过(全仓 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),source 已有 meeting。
1.4 段 ③:EMAI 现状
| 项 | 现状 | 位置 |
|---|---|---|
| 链路 | 客户端 直连百炼 multimodal-dialog WebSocket,服务端不在对话链路上 |
bailian_multimodal_service.dart:113 |
| 身份 | client_info.user_id = uid(游客回退 eaimar-guest);MCP 身份靠 user_getmcptoken 签的 2h 令牌,按工具名嵌套塞进 biz_params.user_defined_params |
:277-301、api_getmcptoken.go |
| 需要令牌的工具清单 | 客户端写死 _mcpAuthTools(7 个),服务端加工具必须同步改 |
:583-591 |
| 跨会话记忆 | dialog_id 由 Started 事件给、只在本连接内回传,不持久化;每次重连是新对话。聊天记录本地 200 条 |
:399,474、emai_chat_store.dart |
| 端侧指令 | 只有 SET_clock 落地(双写本地 + memory_add);SEND_message/MAKE_A_PHONE_CALL 在 _declined;其余 unsupported。模块注册表的 directives 机制接好了但没有任何模块声明 |
assistant_directive_service.dart:62-134、module_registry.dart:88 |
| 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/ |
| MCP 鉴权 | 已落地:header JWT 或 auth_token(aud=mcp)解 uid,参数 uid 不一致直接拒,无密钥则全拒 |
auth.go:92-162 |
| MCP 写工具 | 一个都没有(add_user_task 代码里根本没 AddTool,是死的) |
tool_allhelp_task.go:31-35 |
| 知识库 | 没有。纪要检索是自家 MySQL ngram 全文(词面匹配),换向量方案的触发条件写在工具头注释里 | tool_meeting.go:21-53 |
| 多 agent | EMAI 与 Smartcar 车载助手是两个百炼应用(bailianAppId 在 data 层 AgentModule),设备唤醒按厂商品类选;共用 EmaiController/BailianMultimodalService/AssistantDirectiveService |
agent_module.dart:52、device_ai_session_service.dart:52-72 |
| 后台 | agent_config 表两台机都是 0 行,页面 9-12 已删;百炼 app_id/工具绑定当前不受后台管,在百炼控制台 + 客户端常量里 |
menus.ts:68-71 |
⚠️ MCP 工具注册的四处名字对不上(顺手要修,否则新工具也会同样静默失效)
AddTool 只登记 Groups 里点了名的工具(module.go:163-171),名字对不上就不报错、直接不暴露:
| 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)
二、架构约束(决定实现方式的硬事实)与设计原则
2.1 现有架构里与本文直接相关的 9 条
| # | 事实 | 对本设计的约束 |
|---|---|---|
| A1 | gateway / home / api / mcp / timer 五个进程打在一个镜像、一个容器,entrypoint 见任一 pid 退出就杀全部(entrypoint.voitrans.sh:46-48) | 新增模块方法的 Init 不能返回 error;存量回填放 Start 里起 goroutine;任何建表/回填失败只记日志 |
| A2 | echomeet 与 memory 同在 home 进程,互相通过 comm.IEchomeet / comm.IMemory 接口调用,不 import 对方(comm/module.go:143) |
需求 ② 的四个钩子是进程内接口调用,不走 RPC、不走队列 |
| A3 | mcp 是独立进程(rpcx 集群服务,端口 7300 HTTP / 7004 RPCX),只配了 MySQL,没有 Redis、没有 Postgres(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、comp_httproute.go:103-135) |
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) |
mcp.yaml 的 Groups 加工具名、home.yaml 的 memory 段,两台机各改一次并重启容器;本文不新增 .env 变量(复用 MCP_TOKEN_KEY/GATEWAY_TOKEN_KEY) |
| A7 | 算力系数存业务库 config 表,comm.LoadComputeRates 30s 缓存;console 「会员与算力」页用应用连接读写(api_compute.go:29-73,注释明写 console 不能用包级 mysql) |
新系数 COMPUTE_RATE_SUMMARY 三处同改:comm/compute.go 常量与 ComputeRates、api_compute.go 的 key 列表、compute.vue 一行 |
| A8 | 会议公共模板在 Postgres 公共库、按 app_name 隔离(echomeet/model.go:101-121),新应用要先在后台「从其它应用复制」 |
默认模板解析必须带 comm.AppName();查不到时自动链路只建记录不起任务并记一条日志,不能让翻译结束流程报错 |
| A9 | 客户端登录时 Synchrodata 用 echomeet_getallrecords 拉全量整行(含 original/translate/summary),服务端有 getrecordbriefforuid 但没接线(model.go:232) |
翻译会话自动进纪要会让记录数上一个量级;二期要把 getallrecords 换成 brief + 按需拉详情(§4.5) |
2.2 设计原则(三段共用)
- 不新造第二套「记录」。翻译会话进纪要就是一条
echomeet_record,进拾忆就是一条memory_item;不建translation_session表、不建timeline表。 - 服务端为真相源,客户端本地是缓存。翻译历史现在只活在 GetStorage,重装即丢;进纪要之后自然有了服务端副本,这是副产品不是目标。
- 拾忆存索引不存正文。
memory_item.category=meeting只放标题/时间/时长/概览摘要 + 指回记录的source_id,正文永远在echomeet_record。两处正文必然漂移。 - 能让模型看到结果的写操作走 MCP,看不到结果的设备动作走端侧指令(§5.2)。
- 失败隔离不变:任何附赠链路(进拾忆、抽待办、入索引)失败只记日志,不改主记录状态。
- 凡是「名字对不上就静默失效」的地方补一道启动自检(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)会把 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)与 _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)对这条链路是反的:转写记录不需要音频。要么绕过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;同步路径的 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)。翻译会话在 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 里没有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组件的内存快照(模板缓存本就存在,NATSRpc_ModifyEchomeetTemplate热更),不加查库。 - 本应用一条公共模板都没有时(新部署没复制模板):
starttask返回TemplateNotFound,客户端自动链路把它当成「记录已建、总结未起」静默处理并记日志,详情页「生成纪要」照常可手动重试。翻译结束流程不能因此弹错。 - 以后要在后台加「默认模板」勾选,只改这个解析函数,客户端不用动。
3.6 客户端展示与老包兼容
- 详情页「转写」Tab 现在
tasktype >= 3才解锁(meeting_details_view.dart:354-393),转写记录建好时 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 白名单加一行,不改表):
| 字段 | 取值 |
|---|---|
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):
// 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」,理由是延迟低、离线能先落本地。本文推翻这条,理由:
- 模型看不到端侧指令的执行结果。
tool_calls下发即结束,模型在同一帧里已经说了「已经帮你定好了」——CLAUDE.md 记录的「取消闹钟 / 记待办不下发指令却回已取消」就是这种结构的必然产物。MCP 工具的返回会进入模型的下一句话,「记好了,明天下午三点」是真的。 - 「离线先落本地」不成立。指令只在百炼会话建立时才收得到,会话本身就要联网;不存在「离线拿到指令」这回事。
- 耳机在后台唤醒这个场景两种方式一样:会话都跑在手机进程里。
- 写路径在服务端,幂等、限流、注销清表全在一处;端侧指令那套
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),只是调用方从 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),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 已有这个返回)。 -
RPC 超时 5s;超时/失败回给模型「没记上,请再说一次」,不要回「已记好」。
-
今天是几号由服务端给:工具入参日期允许省略,mcp 按
user_getmcptoken时上报并签进令牌 claims 的tz折算「今天」(令牌 2h 内时区不会变;不查库)。不能靠模型自己算日期——它不知道今天几号,SET_clock那次03:30的坑就是这么来的。 -
幂等:见上,靠
client_key唯一索引;同一句话被模型重试两次只会落一条。 -
写工具的
title_keyword匹配命中多条时不动手,返回候选让模型追问——宁可多说一句,不能删错。 -
写工具的错误码走
pb.ErrorCode5201 段(记忆中心已开),返回给模型的是可朗读的中文短句,不是 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)。持久化到 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,到了再评估。原因复述一遍免得再议:对话协议里没有入口告诉知识库「只检索这个用户的文件」,隔离只能落在我们自己这一侧;MCP 工具刚好就是这一侧。 - 翻译会话进纪要之后自动进入同一个检索范围(
echomeet_record.summary有全文索引),不用另做。灵感/待办的关键词检索由get_memory_items加keyword参数覆盖(title/detailLIKE,量小够用)。
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 |
兜底已发(记忆中心一期) |
待确认(产品/运营)
- 翻译会话默认自动生成纪要还是默认只存转写?默认自动会持续消耗算力与 VIP 用户的耐心(每次通话结束都多一条纪要);默认关则功能等于没做。建议默认开 + 30s 门槛,两周后看
user_getcompute的 Summary 用量再定。 COMPUTE_RATE_SUMMARY的初值。建议会议系数的 1/2。- 记忆写操作改走 MCP(§5.2)是否接受——它推翻记忆中心文档 §8.3,且意味着
SET_clock那套本地双写在一个版本后退役。 START_feature要不要让香薰能拉起手机上的翻译页:技术上是同一条链,但用户在车里对着香薰说「开始通话翻译」、手机自己跳页,体验是否合理。- 拾忆里「来源会议已删除」的待办要不要跟着删。建议不删(已经是用户的任务)。
- 联网搜索国内走百炼内置、海外走 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 |