Omni Launcher 接入¶
本页面向同一 Android 设备上的 Launcher 接入开发者。语音助手负责录音、识别、业务编排和本地播放,Omni Launcher 负责对话面板、动作卡片与应用承载。
当前默认启用 Headless:语音助手保留逻辑会话,不添加自己的悬浮窗。整体职责见架构说明,消息字段见接口参考。
连接参数与角色¶
| 项目 | 当前实现 |
|---|---|
| gRPC 服务端 | 语音助手 OmniAgentGrpcServer |
| gRPC 客户端 | Omni Launcher |
| 设备内地址 | 127.0.0.1:50056 |
| 服务名称 | omniagent.OmniAgentService |
| 协议源码 | core/grpc-server/src/main/proto/OmniAgentProto.proto |
| 传输 | 设备回环地址上的明文 gRPC,当前没有 TLS 配置 |
| 服务端入站消息上限 | 10 MiB |
| HTTP/2 keepalive | 周期 30 秒,超时 10 秒,允许无活跃调用时探活 |
以当前实现为准
旧工程文档中的 50055 已不适用于当前 Omni 服务;当前默认端口是 50056。Mock gRPC 与云端连接属于其他链路,不能混用端口。
客户端与服务端必须使用兼容的 Proto。升级时保留已有字段编号,特别注意新增的 assistant_state = 12,不要用旧客户端的多信号组合逻辑代替当前 UI 状态字段。
建立连接¶
建议在应用级连接管理器中维护一个 Channel,以及三条独立长连接。页面仅订阅管理器输出的状态,避免每次进入页面重建连接。
sequenceDiagram
participant O as Omni Launcher
participant V as 语音助手 gRPC
participant E as 对话事件总线
O->>V: SubscribeServerStream(client_id)
V-->>O: Heartbeat / ServerPushMessage
O->>V: 建立 ToastStream 双向流
O->>V: 建立 AppCtrl 双向流
E->>V: 识别、播报、业务事件
V-->>O: assistant_state / chat_text
V-->>O: ToolsConfirmToast
V-->>O: AppCtrlRequest
O->>V: AppCtrlResponse(原 request_id)
最小连接片段使用 gRPC Kotlin 生成代码;scope 应由应用级连接管理器持有,并在管理器关闭时取消。
val channel = ManagedChannelBuilder
.forAddress("127.0.0.1", 50056)
.usePlaintext()
.build()
val stub = OmniAgentServiceGrpcKt.OmniAgentServiceCoroutineStub(channel)
val subscription = StreamSubscription.newBuilder()
.setClientId("launcher-main")
.build()
scope.launch {
stub.subscribeServerStream(subscription).collect { message ->
// 按 messageTypeCase 转交文本、状态、心跳和错误处理器。
onServerMessage(message)
}
}
这个片段展示连接和订阅方法,onServerMessage 是接入方自己的分发入口。异常应由外层连接管理器记录并触发重连;关闭时取消所有流,并调用 Channel 的 shutdown()。
订阅和重连的实际边界¶
client_id为空时服务端生成标识;同一标识重新订阅会关闭旧 Session,因此同一客户端重连应复用稳定标识,不同客户端应使用不同标识。- Proto 支持
subscribed_types,但当前ServerStreamSession没有按类型过滤。客户端必须自行忽略不需要的消息。 heartbeat_interval和max_reconnect_attempts当前没有用于服务端行为,应用层心跳固定每 30 秒生成一次。- Session 创建时立即尝试发送心跳,但使用无 replay 的 SharedFlow,首条心跳存在早于收集器就绪的可能;不要将“未立即收到首条心跳”等同于连接失败。
- 当前不重放历史文本,也不发送完整 UI 状态快照。重连后依赖后续事件;
QueryOmniAgentStatus返回的是内部 Agent 状态,不能恢复完整面板状态。 - 客户端应针对流完成和传输异常实施自己的有上限退避重连;不要期望请求字段替客户端执行重连。
UI 只消费 AssistantState¶
assistant_state 是对话面板的状态来源。agent_status、tts_event 和 vad_event 留作辅助信息,不应再共同推算 Launcher UI 状态。
| 值 | 展示含义 | 当前主要触发条件 |
|---|---|---|
ASSISTANT_STATE_IDLE |
收起或空闲 | 会话停止、退出、中断、错误 |
ASSISTANT_STATE_LISTENING |
已唤醒、等待输入 | KWS 唤醒;TTS 后两项就绪条件同时满足 |
ASSISTANT_STATE_THINKING |
等待回复 | 非空最终识别文本 |
ASSISTANT_STATE_SPEAKING |
正在播报 | Tts.StreamStarted |
stateDiagram-v2
[*] --> IDLE
IDLE --> LISTENING: KWS 唤醒
LISTENING --> THINKING: 非空 ASR final
THINKING --> SPEAKING: TTS 开始
SPEAKING --> LISTENING: 本地播放完成且 SDK 已可聆听
LISTENING --> IDLE: 停止或退出
THINKING --> IDLE: 中断或错误
SPEAKING --> IDLE: 中断或错误
TTS 数据流结束不代表扬声器播放结束。只有本地 PlayFinished 和 SDK 的 Listening 回调都到达,服务端才从播报态进入跟随聆听态;两条回调先后顺序不固定。
当前不向 Launcher 发送 TTS PCM,音频由语音助手本地播放。Launcher 应继续处理 TTS_CTRL_START、TTS_CTRL_STOP,但不能因为收不到 tts_audio 判定播报失败。
退出词流程另有兼容处理:立即进入 IDLE 后,约 1 秒再推送 THINKING → IDLE,以适配现有 ASR 气泡隐藏逻辑。客户端应按状态事件渲染,避免把这次 THINKING 当成一次新的云端请求。
文本与动作卡片¶
用户文本通过 CHAT_TEXT_TYPE_USER 发送,识别中 is_final=false,最终结果 is_final=true。AI 文本通过 CHAT_TEXT_TYPE_AI 发送,可来自 LLM、业务流增量或可展示的业务最终结果。
同一段文本需要结合角色、chat_id、内部 message_id 和 is_final 更新。ASR partial 是当前识别文本,业务 StreamingDelta 是增量;不能将所有非 final 文本统一无条件拼接。接入方需要与使用的事件来源对齐文本策略。
ToastStream 当前也承载动作卡片,不仅是确认弹窗。业务结果被包装为 TOAST_TYPE_NOTIFY,常用 extras 如下:
| 键 | 示例/含义 |
|---|---|
functionName |
业务函数名 |
processType |
业务类型名,如 MEDIA_AGENT |
showOnUi |
字符串 true 或 false,控制结果文字展示 |
dataType |
内部业务数据类型名称,仅用于识别类型 |
dialogId |
对话关联标识 |
persistent |
普通业务卡片为 false;Mock 常驻卡片为 true |
dataType 不包含完整业务 JSON;结构化支付等内容还有自己的会话存储与页面链路,不能从此字段恢复业务对象。同一轮按业务类型与函数名去重;GRPC_NOTIFY 和明确抑制 Omni 分发的结果不会生成动作卡片。
AppCtrl 指令与回执¶
这里的方向容易误解:Launcher 建立 AppCtrl RPC,但指令由语音助手发送给 Launcher,回执由 Launcher 发回语音助手。使用 Flow<AppCtrlResponse> 持续上传回执,并收集服务端 Flow<AppCtrlRequest>。
app_id |
当前用途 |
|---|---|
navigation |
导航应用与 TaskView |
media |
媒体应用 |
rednote |
小红书展示 |
aiwidget |
AI Widget |
ai_order |
点单支付页面 |
ai_tickets |
门票页面 |
常规启动请求携带 parameters["input_text"]。需要继续输入时使用 APP_CTRL_INPUT 的 text_input,并通过 parameters["process_type"] 标明处理类型。
{
"app_id": "navigation",
"request_id": "request-demo-001",
"command": "APP_CTRL_START",
"parameters": {"input_text": "导航到附近的停车场"}
}
配套回执示例使用 Proto 字段名表达含义;实际通过生成的 Protobuf 消息传输:
{
"type": "APP_CTRL_RSP_BY_REQ",
"request_id": "request-demo-001",
"app_id": "navigation",
"success": true,
"message": "页面已就绪",
"data": {"inTaskView": "true"}
}
顺序与失败处理¶
- 服务端生成 UUID 请求标识,最长等待回执 10 秒,超时返回
null并记录日志。 - 当前兼容空
request_id回执,但新客户端必须回传原始标识;空标识可能错误关联并发请求。 - 导航仅当回执
data["inTaskView"] == "true"时执行本地待执行命令;否则发送APP_CTRL_INPUT,由对端按输入继续处理。 - 常规分支在等待启动回执后执行待执行命令,当前并不统一以
success=true作为执行门槛。超时日志与command_result均不等同于业务执行成功。 - 点单支付先执行命令并等待结构化卡片,再启动
ai_order;门票在SHOW_SPOTS且景点非空时启动ai_tickets。 - AppCtrl/Toast 当前使用共享热流,没有按客户端独立路由或离线队列。建议只保留一个负责控制的 Launcher 消费者,并在业务触发前建立双向流。
RPC 能力边界¶
StartOmniAgent、StopOmniAgent 当前仅更新 AgentControllerImpl 的运行标记及状态流,没有调用真实录音或 SDK 会话启停。SyncDemoMode 当前只记录参数,返回成功不代表演示模式已经切换。
真实会话由 VoiceAssistantService、唤醒逻辑与对话协调器驱动。首次联调请先完成构建与运行,再验证完整语音链路。
联调排障¶
| 现象 | 检查顺序 |
|---|---|
| 连接被拒绝 | 服务端启动日志 → 端口 50056 → 是否在同一设备回环地址 |
| 只有心跳、没有文本 | 服务是否初始化事件订阅 → 真实会话是否开始 → ASR 是否产生结果 |
| 面板提前退出播报态 | 是否错误把 StreamCompleted 或旧状态组合逻辑当作播放完成 |
| 导航退回文本输入 | 回执是否原样关联 request_id,inTaskView 是否字符串 true |
| AppCtrl 超时 | 双向流是否已经建立、是否断开、回执是否丢失或错误关联 |
| 重连后缺少旧文本 | 当前没有历史 replay,属于实现边界 |
日志定位类:OmniAgentGrpcServer、OmniAgentServiceImpl、ServerStreamSession、GrpcEventBroadcaster、AgentControllerImpl、AppCtrlDispatcher。采集方法见调试与维护。