从“演示如何下单”到可控的终端引导:Captain 显示点单指南 Skill 技术实践
在收银端,用户说“演示如何下单”时,关键不是让模型生成一段操作说明,而是让收银界面真正进入一个可观察、可退出、不会误触发真实交易的引导状态。Captain 的“显示点单指南”Skill,也就是 show_ordering_guide,就是为这个场景设计的:模型负责理解意图,服务端负责生成受控流程,Android 收银端负责展示高亮并等待用户操作。
本文结合收银端演示截图,拆解这个 Skill 的定义、路由、Client Bridge 执行链、五个引导步骤、状态回传和安全边界。
先看最终体验:引导覆盖在真实收银页面之上
截图中的智能中心位于收银界面右侧。用户发起“演示如何下单”后,智能中心显示“正在客户端上执行”,同时收银页面被半透明遮罩覆盖,只保留当前步骤对应的可操作区域。圆形聚光区域依次指向:
- 左侧的商品大类入口。
- 商品卡片。
- 购物车中的确认下单入口。
- 结算页的 POS 支付入口。
- 最终订单确认按钮。
这是一种客户端辅助,而不是远程代操作。引导服务告诉客户端“下一步应该突出哪个控件”,客户端根据自己的页面结构定位目标,并由收银员完成点击。第一张截图还揭示了一个重要的运行时事实:智能中心在请求已交给收银端后显示“正在客户端上执行”,它等待的是客户端结果,而不是在服务端模拟点击。

sequenceDiagram
participant O as 操作员
participant C as 智能中心
participant R as Captain Runtime
participant B as Client Bridge
participant P as Android 收银页面
O->>C: 演示如何下单
C->>R: 创建 Skill Operation
R->>R: 路由、授权与 Schema 校验
R->>B: 创建 ui.show_guide 请求
B->>P: 下发固定引导流程
P-->>O: 遮罩、高亮、步骤文案
O->>P: 按当前步骤操作
P-->>B: completed / failed / disconnected
B-->>R: 验证 nonce 并更新 ToolCall
R-->>C: 更新 Operation 与状态卡片
Skill 的定位:固定工作流,而不是自由规划
关键属性如下:
| 属性 | 当前值 | 设计含义 |
|---|---|---|
| slug | show_ordering_guide | 稳定的 Skill 标识,用于路由、快捷操作和审计 |
| skill_type | client_assist | 结果由客户端展示和辅助操作 |
| execution_mode | configured_workflow | 步骤由服务端配置,不交给模型临时编排 |
| system_keys | cashier | 只允许收银系统使用 |
| client_types | android | 只允许 Android 收银客户端执行 |
| max_steps | 1 | Skill 本身只发起一个引导 Capability |
| max_risk_level | L1 | 低风险、只读的界面辅助动作 |
| Capability | ui.show_guide | 通过 Client Bridge 启动引导 |
这里有一个容易混淆的地方:Skill 的 max_steps 为 1,并不代表界面只有一步。它表示 Operation 只执行一个 Capability;这个 Capability 的参数中携带了一个包含五个 UI 节点的引导流程。这样既能把一次完整引导作为可追踪、可取消的客户端请求,也避免模型逐步决定下一处页面要点击什么。
五个步骤如何对应截图
服务端的默认工作流 main.ordering.checkout 包含五个步骤:
| 顺序 | Step ID | 定位方式 | 截图中的交互 |
|---|---|---|---|
| 1 | first_category | resolver: main.order.first_category | 高亮商品大类,提示“点击进入该商品大类” |
| 2 | first_food | resolver: main.order.first_food | 高亮第一个商品卡片,提示“点击将该菜品加入购物车” |
| 3 | cart_place_order | target_id: main.cart.action.place_order | 高亮购物车确认入口,提示校验库存并进入订单确认页 |
| 4 | settle_pos | target_id: main.settle.pay_method.unionpay | 高亮支付方式入口,演示选择 POS 或银联支付 |
| 5 | settle_confirm | target_id: main.settle.action.confirm | 高亮最终确认按钮,wait_for_dismiss 为 false |
这五步不是模型逐步生成的推理轨迹,而是一条由服务端下发、由收银端执行的固定界面流程:
flowchart LR
A["启动点单指南"] --> B["first_category:解析首个商品大类"]
B --> C["first_food:解析首个商品卡片"]
C --> D["cart_place_order:高亮确认下单"]
D --> E["settle_pos:高亮 POS 支付"]
E --> F["settle_confirm:高亮最终确认"]
F --> G["客户端回传引导结果"]
步骤 1:进入商品大类
客户端用 main.order.first_category 解析当前菜单中第一个可用商品大类,而不是依赖固定屏幕坐标。第二张截图中,聚光区域落在左侧“餐食”入口,文案提示操作员点击进入该商品大类。菜单调整后,解析器可以继续返回新的可用入口。

步骤 2:选择第一个商品
进入大类后,客户端用 main.order.first_food 解析可售商品卡片。第三张截图高亮“茶咖元气早餐”商品卡片,并引导操作员点击将其加入购物车。这里依然不由服务端指定商品的屏幕坐标或价格,避免菜单内容变化后引导失效。

步骤 3:确认下单
商品加入购物车后,引导改用稳定目标 ID main.cart.action.place_order。第四张截图将聚光区域定位到右下角“确认”按钮,说明该动作会校验购物车库存并进入订单确认页。客户端只展示和等待操作,真实订单状态仍由收银端自身业务流程决定。

步骤 4 与 5:选择支付方式并确认订单
结算页依次使用 main.settle.pay_method.unionpay 与 main.settle.action.confirm 两个稳定目标 ID。第五张截图展示最终确认按钮的聚光效果;在此之前,客户端会引导选择 POS 或银联支付。整个流程演示的是收银端原本就有的交互,Guide 不会替用户选择支付方式、提交订单或完成扣款。

前两个步骤使用 resolver,因为商品大类和商品卡片的位置会随门店菜单变化;后三个步骤使用稳定的 target_id,因为结算页的功能控件属于固定 UI 契约。动态内容用客户端解析器,稳定控件用版本化目标 ID,是这套终端引导的核心设计。
最后一步设置 wait_for_dismiss 为 false,客户端在展示最终按钮后不因引导遮罩被关闭而错误地把界面动作理解成服务端已经执行了支付。引导的完成表示“客户端已经调度并展示了流程”,不表示订单或支付由 Runtime 代为提交。
为什么流程参数必须由服务端生成
ui.show_guide 的输入 Schema 要求参数必须是 flow 对象,并限制:
- flow_id 和 display_name 必须满足长度约束。
- requirements 只能声明已知的 scan_service_enabled。
- 步骤数量必须在 1 到 32 之间。
- 每个步骤必须提供 target_id 或允许的 resolver。
- resolver 只能是 main.order.first_category 或 main.order.first_food。
- 每个步骤都不允许额外字段。
因此,模型不会因为误判或 Prompt 注入而临时构造未知页面路径。模型可以判断“用户想看点单流程”,但不能决定跳转到未知路由、执行支付动作或向客户端注入未经审核的逻辑。
configured_workflow 的执行步骤来自数据库中的 Skill 配置;默认 Provisioner 以幂等方式创建或升级 Skill、Capability 和步骤。运营人员可以管理 Skill 生命周期,Runtime 则只执行已发布、已授权的配置。
两种入口,最终走同一条执行链
自然语言入口
当操作员输入“演示如何下单”“带我走一遍点餐流程”或“教我怎么在收银机下单”时,Skill Router 会从已授权候选中选择 show_ordering_guide。路由还会检查:
- 当前 Assistant 是否绑定该 Skill。
- 当前会话是否来自收银系统。
- Client Session 是否为 Android。
- 客户端是否声明了 ui.show_guide。
- 当前 Skill 是否启用且没有超出风险策略。
如果用户只问“怎么点单”,而没有表达演示或带领操作的意图,系统可以优先走知识库回答;“显示点单指南”适用于明确要求演示、学习或逐步带操作的请求。
Quick Action 入口
系统同时提供 show_ordering_guide Quick Action。点击快捷操作后,服务端直接生成同名 Skill 的执行计划,不再依赖一次 LLM Planner 判断。两种入口最后都经过 Candidate Resolver、Capability Policy、Plan Validator 和 Operation Runner,因此快捷入口不是绕过权限的后门,只是减少了意图识别的不确定性。
Client Bridge:从排队到完成的状态机
Client Bridge 请求由 Captain::ClientBridge::Request 创建为 Captain::ToolCall。它的状态迁移必须由服务端控制,客户端不能直接把任意请求标记为成功:
stateDiagram-v2
[*] --> queued: 创建并通过授权和 Schema 校验
queued --> running: 客户端领取请求,服务端签发 nonce
running --> completed: nonce、Capability、输出 Schema 均通过
running --> failed: 客户端失败、断开或输出不合法
queued --> failed: 客户端离线或版本不兼容
queued --> timed_out: 30 秒内未被领取
running --> timed_out: 回传超过过期时间
completed --> [*]
failed --> [*]
timed_out --> [*]
执行过程分为四个阶段。
1. 创建请求
Runtime 先检查 Operation 必须处于 running,再用 Capability Policy 验证 ui.show_guide 是否允许当前账号、收银系统和 Android 客户端使用,最后按输入 Schema 校验 flow。请求会保存参数摘要、幂等键和 30 秒过期时间。
2. 客户端拉取并领取
Android 客户端通过 Client Bridge 拉取待处理请求。DeliveryService 在事务中锁定最早的一条 queued 请求,将其改为 running,生成一次性 nonce,并返回请求信封,其中包含 capability_key、flow 参数和 expires_at。
一次性 nonce 的摘要只保存在服务端,客户端回传原值。这样即使请求 ID 被看见,也不能伪造一个有效的完成回调。
3. 客户端展示引导
收银端收到请求后,根据 resolver 或 target_id 在当前页面找到目标控件,绘制遮罩和聚光区域,并等待操作员完成步骤。截图里的“正在客户端上执行”就是这个阶段的运行态提示;它不是服务器正在模拟点击。
4. 回传并恢复 Operation
客户端回传 completed、failed、unsupported 或 disconnected,并附带 Capability、nonce 和结构化结果。服务端在行锁内校验请求仍为 running、Capability 与 nonce 匹配、响应状态合法、输出符合 Schema,然后更新 ToolCall。完成后,Skill Runner 恢复 Operation 并生成最终输出。
若 30 秒内没有完成回传,过期任务会转为 timed_out;客户端离线或版本不支持能力,则分别记录断开或版本不兼容错误,不会让智能中心无限等待。
异常处理不是在客户端吞掉失败后结束,而是沿同一条桥接链回到 Operation。下面的分支图说明了智能中心为什么能区分正常完成、离线、版本不支持与超时:
flowchart TD
A["创建 ui.show_guide ToolCall"] --> B{"客户端在线且版本支持?"}
B -- 离线 --> C["failed: client_bridge_disconnected"]
B -- 不支持 --> D["failed: client_bridge_unsupported"]
B -- 支持 --> E["queued,等待客户端领取"]
E --> F{"30 秒内领取?"}
F -- 否 --> G["timed_out"]
F -- 是 --> H["running,签发 nonce"]
H --> I{"回传是否有效?"}
I -- nonce 或 Schema 无效 --> J["failed"]
I -- 客户端执行失败 --> J
I -- 完成 --> K["completed,恢复 Skill Runner"]
幂等与重复点击:为什么不会重复创建引导
“显示点单指南”虽然是只读引导,但仍然需要幂等,因为消息队列可能重复投递,客户端也可能重复拉取。当前实现有三层保护:
- Operation 使用账号级幂等键标识一次 Skill 执行。
- 每个步骤按 operation_key 与 step_index 生成稳定 ToolCall 幂等键。
- 相同 Operation 再次执行时,ClientBridge::Request 直接复用已有 ToolCall,不再次创建客户端请求。
客户端回调也不是“收到一次就更新一次”:ToolCall 只有在 running 状态才接受响应,已处理请求再次回调会返回 client_bridge_response_already_processed。服务端还会校验 nonce、Capability 和输出 Schema,防止旧回调或伪造结果改变 Operation 状态。
这套幂等保证的是“不重复产生业务请求”,并不保证客户端界面不会因用户主动重新打开而再次展示引导。若操作员明确重新发起一条新的消息,它应当拥有新的 Operation 和新的幂等上下文。
风险边界:引导可以做什么,不能做什么
ui.show_guide 的 Capability 被定义为:
- Channel:client_bridge。
- Risk:L1。
- System:cashier。
- Client type:android。
- Metadata:只读。
它只能启动一个服务端配置的交互指南,不能直接修改订单、提交付款或改变打印机状态。即使路由预设改成“行动优先”,也不会绕过 Capability 授权、客户端范围、风险等级和 JSON Schema 校验。
如果未来需要“自动选择商品”“自动提交订单”或“自动发起支付”,这些都应设计成新的 Capability,并单独定义输入输出、风险等级、确认策略和幂等语义,不能把它们悄悄塞进 ui.show_guide。
失败处理和用户体验
对操作员而言,技术状态应被翻译成可理解的反馈:
| 技术状态 | 用户可见反馈 | 处理建议 |
|---|---|---|
| queued | 正在准备点单指南 | 等待客户端领取请求 |
| running | 正在客户端上执行 | 在收银页面完成当前高亮步骤 |
| completed | 点单指南已完成 | 收起遮罩,保留结果摘要 |
| client_bridge_disconnected | 收银端暂时离线 | 检查客户端连接后重试 |
| client_bridge_unsupported | 当前客户端版本不支持 | 升级收银端客户端 |
| timed_out | 引导等待超时 | 重新发起指南或检查页面状态 |
| failed | 点单指南启动失败 | 展示失败原因,不执行任何真实订单动作 |
状态卡片应同时保留 Skill 计划和当前步骤,方便操作员知道系统正在等待什么;取消按钮则应触发 Operation 取消,而不是只关闭前端面板。
如何验证这个 Skill
建议用三类用例验证。
路由用例
- “演示如何下单”应命中 show_ordering_guide。
- “教我怎么在收银机下单”应命中同一 Skill。
- “商品规格怎么选”应优先走知识库,不应直接启动客户端引导。
- “检查 dine_in 打印机状态”不应误命中点单指南。
客户端能力用例
- Android 收银客户端在线且声明 ui.show_guide 时,能收到请求。
- 客户端离线时,Operation 快速进入断开状态。
- 客户端版本不支持时,返回明确的升级错误。
- 请求过期后,迟到的完成回调不能改变状态。
流程完整性用例
- 五个步骤按固定顺序展示。
- 前两个动态解析器能定位当前菜单中的大类和商品。
- 结算页三个固定目标能正确高亮。
- 最终确认步骤不会误触发真实支付。
- 重复消息不会产生第二个并发引导请求。
结语:把“演示”做成一个受控的产品能力
显示点单指南的核心价值,是把一个看似简单的“教我点单”请求拆成四个稳定边界:
- 语言理解边界:识别用户确实要求演示,而不是咨询商品规则。
- 工作流边界:服务端只执行已配置、已审核的五步流程。
- 客户端边界:只有匹配的 Android 收银端才能展示指南。
- 业务副作用边界:引导可以高亮和等待,但不会替用户提交订单或支付。
这也是 Agent Runtime 设计终端 Skill 的通用方法:让模型负责理解,让服务端负责约束,让客户端负责呈现,让真实业务动作继续经过独立的 Capability、风险和确认体系。
评论