跳转至

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_intervalmax_reconnect_attempts 当前没有用于服务端行为,应用层心跳固定每 30 秒生成一次。
  • Session 创建时立即尝试发送心跳,但使用无 replay 的 SharedFlow,首条心跳存在早于收集器就绪的可能;不要将“未立即收到首条心跳”等同于连接失败。
  • 当前不重放历史文本,也不发送完整 UI 状态快照。重连后依赖后续事件;QueryOmniAgentStatus 返回的是内部 Agent 状态,不能恢复完整面板状态。
  • 客户端应针对流完成和传输异常实施自己的有上限退避重连;不要期望请求字段替客户端执行重连。

UI 只消费 AssistantState

assistant_state 是对话面板的状态来源。agent_statustts_eventvad_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_STARTTTS_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_idis_final 更新。ASR partial 是当前识别文本,业务 StreamingDelta 是增量;不能将所有非 final 文本统一无条件拼接。接入方需要与使用的事件来源对齐文本策略。

ToastStream 当前也承载动作卡片,不仅是确认弹窗。业务结果被包装为 TOAST_TYPE_NOTIFY,常用 extras 如下:

示例/含义
functionName 业务函数名
processType 业务类型名,如 MEDIA_AGENT
showOnUi 字符串 truefalse,控制结果文字展示
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_INPUTtext_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 能力边界

StartOmniAgentStopOmniAgent 当前仅更新 AgentControllerImpl 的运行标记及状态流,没有调用真实录音或 SDK 会话启停。SyncDemoMode 当前只记录参数,返回成功不代表演示模式已经切换。

真实会话由 VoiceAssistantService、唤醒逻辑与对话协调器驱动。首次联调请先完成构建与运行,再验证完整语音链路

联调排障

现象 检查顺序
连接被拒绝 服务端启动日志 → 端口 50056 → 是否在同一设备回环地址
只有心跳、没有文本 服务是否初始化事件订阅 → 真实会话是否开始 → ASR 是否产生结果
面板提前退出播报态 是否错误把 StreamCompleted 或旧状态组合逻辑当作播放完成
导航退回文本输入 回执是否原样关联 request_idinTaskView 是否字符串 true
AppCtrl 超时 双向流是否已经建立、是否断开、回执是否丢失或错误关联
重连后缺少旧文本 当前没有历史 replay,属于实现边界

日志定位类:OmniAgentGrpcServerOmniAgentServiceImplServerStreamSessionGrpcEventBroadcasterAgentControllerImplAppCtrlDispatcher。采集方法见调试与维护