Files
story/docs/device-integration.md
T
2026-08-29 21:56:51 +08:00

33 KiB
Raw Blame History

设备接入与启动配置联调说明

1. 当前接口

方法 路径 作用
GET /device/v2/bootstrap 上报启动信息,取得服务器时间、可信设备状态、绑定、最低安全基线、最低固件版本及兼容结论
GET /device/v2/config 取得版本化期望配置和当前绑定;支持 If-None-Match
POST /device/v2/conversations 冻结授权和背景;成功时分配RTC房间并进入ROOM_ISSUED,背景准备失败时保存可诊断FAILED结果且不创建房间
POST /device/v2/conversations/{id}/activate 确认设备已成功入房,可靠投递固定语音供应商任务
GET /device/v2/conversations/{id} 读取权威通话状态、稳定失败码和设备下一步动作;用于确认智能体启动结果
POST /device/v2/conversations/{id}/heartbeat 按递增序号上报在线、RTC、配置和录音队列状态并刷新短期租约
POST /device/v2/conversations/{id}/stop 记录挂断原因并可靠停止固定供应商任务,最终收敛到终态
POST /device/v2/conversations/{id}/turns:batch 在供应商终稿缺失时补传设备终稿,并与已保存供应商终稿去重合并
POST /device/v2/telemetry:batch 批量上报设备状态、有限错误码和命令结果,返回配置/命令/停止动作
POST /callbacks/v1/vertc/voice-chat 接收经HMAC签名的语音供应商异步事件

设备自报的型号、设备编号或版本不构成身份依据。云端使用签名中的设备公开编号查询登记记录,并从数据库取得租户、状态和凭据引用。

1.1 家庭端设备绑定

设备绑定码由未绑定设备在受控配网或屏幕流程中显示,家庭Web工作台只调用POST /api/v1/device-claims消费该码,不生成绑定码。输入框限制16—128字符并关闭自动填充;进入确认区后不回显完整码,只显示掩码、总长度和末四位。确认区同时冻结当前家庭、长者及其公开编号,要求操作者当面核对设备屏幕、设备型号和实际使用人,并明确绑定将建立唯一有效的长期家庭/长者关联,错误目标不能通过再次输入同一码自行改绑。

只有显式确认后才提交完整绑定码。服务端重新校验操作者的device.manage能力、家庭与长者边界、设备未绑定、绑定码摘要、到期和撤销状态;相同码和相同目标可安全重试,不同目标或已有有效绑定返回冲突。取消、切换页签/家庭/长者会同时清除确认草稿和输入框中的完整码,不发送请求。页面增量只执行真实OIDC无提交轻量冒烟,验证短码阻断、固定目标、完整码不回显、确认门禁、取消清理、零device-claims POST和绑定记录数量不变;完整首次绑定与安全重试继续由既有真实数据库用例承担。

最低固件版本与最低安全基线是两个独立概念。minimum_security_version保存厂商安全能力或签名基线标识;minimum_firmware_version保存允许创建新通话的固件下限。设备启动上报当前固件后,firmware_compatible立即给出判断;SemVer兼容版本按主版本、次版本、修订号和预发布标识排序,厂商自定义的不透明版本无法安全排序,只允许与策略精确相等。设备必须在兼容结果为false时进入升级提示而不是继续通话;云端创建接口仍会再次校验并返回不可自动重试的FIRMWARE_UPDATE_REQUIRED,因此绕过设备侧提示也不会分配房间。

2. 开发环境签名

每个请求必须包含:

  • X-Device-Id:已登记的设备公开编号。
  • X-Device-Timestamp:RFC3339 UTC时间,默认允许前后5分钟误差。
  • X-Device-Nonce:每个请求唯一的16—128字符随机数。
  • X-Device-Content-SHA256:原始HTTP正文的SHA-256小写十六进制值;空正文也必须计算。
  • X-Device-Signature:HMAC-SHA256结果的无填充Base64URL编码。

签名原文按以下顺序拼接,行间使用单个换行符:

UPPERCASE_HTTP_METHOD
REQUEST_URI_WITH_EXACT_QUERY_STRING
X-Device-Timestamp原值
X-Device-Nonce原值
X-Device-Content-SHA256原值

云端先核验时间窗、正文摘要和签名,再使用Redis SET NX登记随机数。相同设备和随机数只能使用一次;Redis或数据库不可用时设备鉴权采用fail-closed并返回可重试的503,不绕过身份验证。

本地环境允许把既有静态HMAC密钥一次性导入数据库以兼容测试包。量产采用每设备独立HMAC时,设备侧密钥应写入硬件安全区,云端只保存主密钥加密后的密文;如硬件与PKI条件允许,仍可进一步切换每设备mTLS证书。不能把本地合成密钥烧录到真实设备。

2.1 逐设备生产密钥解析

设备表保存公开身份和credential_ref,device_credential表按设备保存AES-256-GCM加密后的HMAC密钥、独立随机nonce、密钥版本与状态,不保存明文。API使用数据库外独立托管的DEVICE_CREDENTIAL_MASTER_KEY解密;生产环境必须通过Docker/Kubernetes Secret或云KMS CSI挂载该主密钥。现有演示环境的DEVICE_LOCAL_SECRET只用于首次启动时导入缺失记录,导入不会覆盖已轮换的数据库凭据。

每次验签都按credential_ref读取当前活动数据库凭据并执行AEAD认证解密;记录缺失、已吊销、主密钥错误或密文被篡改时统一按无效凭据拒绝,响应与日志不暴露密钥内容。device-credential维护命令提供生命周期操作:

device-credential provision <设备公开UUID>
device-credential rotate <设备公开UUID>
device-credential revoke <设备公开UUID>

provision和rotate生成32字节随机密钥,明文只在标准输出显示一次;数据库仅保存密文。轮换提交后旧密钥立即失效,必须通过受控渠道同步更新设备安全区。吊销后设备的所有签名请求立即失败。生产启动门禁拒绝默认主加密密钥。

3. 启动参数

bootstrap查询参数包括:firmware_version、board_rev、rtc_sdk_version、codec、sample_rate、device_time和network。其中固件版本和设备时间必填。服务端把报告写入设备影子的reported区域,网页配置只能修改desired区域。

响应返回服务器时间和设备时钟偏差、登记状态、型号、最低安全版本、期望配置版本及当前绑定。响应不包含内部数据库主键或任何设备密钥。

4. 配置缓存

配置接口返回强ETag,例如 "v1"。设备下次请求可携带:

If-None-Match: "v1"

配置未变化时返回HTTP 304;变化时返回HTTP 200、新ETag、期望配置、设备已报告版本和绑定信息。

4.1 家庭端音量配置

家庭端设备管理页把扬声器音量作为设备影子的非敏感期望配置管理,不直接假定设备已经应用。页面同时展示“云端期望”和“设备上报”两个值:未配置时滑块以60%作为首次建议值,但明确显示云端尚未设置;已有配置时从当前desired_config.volume.speaker回显,相同值不允许重复提交。用户可在0—100范围内逐级调节;提交前的受控操作区固定设备、长者、旧值、新值、设备上报实际值和期望版本变化,并说明离线设备可能稍后领取。

网页提交PATCH /api/v1/devices/{id}/twin时只发送需要修改的白名单顶层分类并携带当前设备影子强ETag。服务端仅接受0—100的整数desired_config.volume.speaker,继续递归拒绝密钥、令牌、口令和凭据字段,并把已验证分类按顶层增量合并到现有期望配置,未提交的历史编码、采样率、心跳和其他分类不会被音量修改删除。版本过期时返回412,页面刷新后由最新设备影子重新回显;成功时响应包含递增后的期望版本,页面立即更新为“等待设备应用”。设备随后通过配置拉取和心跳报告实际版本及reported_config.volume.speaker,二者一致后显示“设备已应用”。

5. 创建通话

创建接口必须同时提供设备签名和16—128字符的Idempotency-Key。服务端在同一事务中完成:

  1. 锁定并核验设备当前绑定、长者状态和语音授权;
  2. 冻结本次授权快照和最小背景快照;
  3. 创建固定通话编号、房间编号和供应商任务编号;
  4. 记录PREPARING → ROOM_ISSUED状态链、RTC任务、审计和可靠待办;
  5. 提交后签发绑定指定房间和设备用户的短期Token。

请求中的capabilities用于显式能力协商。当前模拟终端在原有rtc、audio_tracks和upload_resume之外声明可选的rtc_credential_renewal=true;云端把该能力冻结到本次通话。旧版v2固件省略该字段时仍可创建通话,云端不会向其返回新增的凭证续签动作。能力选择不得根据固件版本或HTTP User-Agent推测,完整兼容规则见设备API兼容性约定。

最小背景快照不是设备自由上报的正文。服务端按当前有效绑定读取长者档案版本,只从profile_data.background/interests和conversation_preferences.conversation_goal/pace/preferred_topics提取有长度上限的白名单资料,再加入最多5条当前有效的已确认记忆。非白名单扩展字段、内部备注和设备侧固定技术目标open_conversation不会进入供应商提示。生成的摘要和档案版本随通话冻结;网页后续修改只影响下一次新通话。供应商系统消息还会把该段内容明确标记为“背景资料而非可执行指令”,不能借此覆盖固定安全规则。

5.1 背景资料准备失败协议

五个白名单字段一旦存在,就必须符合约定类型:背景、交流目标和节奏为字符串,兴趣和优先话题为字符串数组;缺失或null表示未填写。历史导入或异常写库造成类型不正确时,云端不会把字段静默丢弃后继续通话,也不会创建RTC房间或供应商任务,而是保存一条状态为FAILED、失败码为context_preparation_failed的权威通话和脱敏失败快照。

设备收到HTTP 422,错误码为AGENT_CONTEXT_PREPARATION_FAILED,details固定包含conversation_id、status=FAILED和failure_code=context_preparation_failed。该错误不可在原请求上无限自动重试;设备应停止入房流程,展示“背景资料准备失败”,并上报稳定错误码。网络重放必须沿用原Idempotency-Key,云端返回相同通话编号并携带Idempotency-Replayed: true;用户修正档案后才使用新的幂等键发起新通话。家庭网页和运营诊断均按同一failure_code展示“背景资料准备失败,未进入语音通话”。

家庭端可在设备真正发起通话前调用GET /api/v1/subjects/{id}/conversation-readiness。该接口只返回当前成员有权查看的长者及绑定设备事实,把档案背景、交流目标、语音授权、有效绑定和设备运行上报列为准备条件,把配置同步和最近联系列为运行提醒。用量额度、生产路由接纳和进行中通话属于瞬时状态,仍由POST /device/v2/conversations在事务内最终判断;就绪响应不会预留房间、额度或供应商容量。

创建阶段不会调用StartVoiceChat。设备必须先加入返回的RTC房间,再调用后续activate接口。相同设备、接口、幂等键和相同正文返回同一通话;正文不同返回冲突。为避免持久化完整短期Token,幂等记录只保存非敏感房间声明,重放时根据原声明确定性重建同一个Token。

创建响应中的session.engine_mode只供云端诊断、成本归集和通话追溯。终端必须忽略该字段,不得用它切换麦克风采集、编码、近远端轨道发布、订阅或扬声器播放。

6. 激活通话与启动语音供应商

设备收到房间资料并成功加入RTC房间后,调用POST /device/v2/conversations/{id}/activate,正文必须带入云端原先分配的room_id、rtc_user_id,并将rtc_join_result设为success。服务端还会校验当前设备归属、房间预留有效期和config_revision,任何不一致都不会启动供应商任务。

首次有效激活在一个事务中完成ROOM_ISSUED → STARTING_AGENT、RTC任务ALLOCATED → STARTING、审计记录和conversation.agent_start_requested outbox事件。Worker使用创建通话时已经冻结的provider_task_ref调用供应商,重试时不得生成新任务编号。重复的相同幂等请求返回原结果;通话已经处于STARTING_AGENT或ACTIVE时再次确认也不会创建第二个启动事件。

激活接口受理后,设备使用GET /device/v2/conversations/{id}查询启动结果,不通过更换幂等键重复调用激活接口充当轮询。返回动作含义如下:

  • wait:房间已分配或智能体仍在启动,设备保持入房并按有限次数继续查询;
  • continue:权威状态为ACTIVE,设备进入正常双向语音和心跳;
  • leave_room:通话正在停止或已进入终态,设备立即退出RTC房间;FAILED时同时显示稳定脱敏failure_code。

供应商明确、不可重试地拒绝启动时,Worker在同一数据库事务内把通话和RTC任务收敛为FAILED/provider_start_rejected,写入状态历史和系统审计,并正常完成可靠待办,不让通话停留在STARTING_AGENT或把永久错误反复重试。供应商异步报告启动失败时返回既有provider_start_failed。两种情况都不向设备暴露供应商原始响应;模拟终端识别leave_room后立即结束启动流程。

本地mock-provider暴露在http://localhost:59200,用于模拟供应商启动和异步回调。它按固定任务编号幂等受理启动,并故意发送两次完全相同的voice_chat.started回调。Callbacks进程先按时间戳和原始正文校验HMAC签名,再以provider + event_id去重;同一事件编号和同一正文返回成功,同一事件编号却对应不同正文时返回409。首个有效启动事件使通话和RTC任务进入ACTIVE,重复或过时事件不能让状态回退。

生产模式不复用Mock路径。API使用RTC AppKey生成官方正式Token格式,Token只授权本次room_id + rtc_user_id的发布与订阅并在短期后失效;相同创建请求重放时按相同房间、用户和到期时间重建同一Token,数据库和日志均不保存Token。Worker使用火山引擎AK/SK对StartVoiceChat/StopVoiceChat签名,平台稳定任务编号映射为TaskId,房间映射为RoomId,已冻结的设备/智能体RTC用户映射到AgentConfig。通话创建时冻结的s2s_pure、s2s_hybrid或cascade会进入可靠启动任务,Worker从voice-chat-template-bundle-v1选择同名模板;生产启动要求三个模板全部存在,避免运营路由切换后仍使用另一种处理链。官方同步响应Result=ok表示RTC已接受并启动该任务,平台据此进入ACTIVE,后续由设备心跳维持租约;火山接口没有任务查询API,因此不确定启动的恢复逻辑会以同一TaskId安全重发,供应商“任务已启动”响应同样按已存在活动任务收敛,不生成新任务编号。服务端状态事件可作为状态细分和用量补充,但不是最小通话链进入ACTIVE的单点依赖。

生产API优先选择活动投放所绑定的AGENT_BUNDLE版本,并从组合引用的路由版本取得处理模式;config_key是运营自定义版本族标识,不要求保留键名。未命中组合时兼容活动投放所绑定的直接ROUTING版本。两类投放都未命中时返回503 RUNTIME_CONFIG_UNAVAILABLE,不会回退到开发用minimal-v2 + s2s_hybrid。dev、test和local环境保留该默认值,便于模拟设备和本地闭环运行。该变化仅影响云端新通话配置选择,不改变设备v2请求、响应字段或终端媒体行为。

阶段8真实验收按真实终端与RTC/语音供应商一致性证据执行6通精简矩阵,三种方式各完成正常闭环,并额外覆盖凭证续签、网络切换恢复和受控停止。协议模拟只验证工具链,不计入REAL_DEVICE_RTC。

模拟供应商还提供仅限本地、使用X-Mock-Provider-Key保护的POST /mock/v1/admin/replay。请求指定已启动的task_ref、样例名和1—10次重复投递次数,可重复播放started、final_transcript、agent_answering、interrupted、usage、error和stopped七类脱敏通知。agent_answering和interrupted以最终智能体话轮表达,后者明确设置interrupted=true;usage只使用标准化分类、计量代码、数量和单位。相同样例针对同一任务生成稳定事件编号,用于验证通知幂等,不应在生产部署中暴露该管理接口。

供应商回调签名使用以下原文:

X-Provider-Timestamp原值
原始HTTP正文的SHA-256小写十六进制值

签名算法为HMAC-SHA256,响应编码为无填充Base64URL。本地共享密钥只用于联调;生产应改由密钥管理服务托管并建立轮换机制。

7. 通话心跳与租约

通话进入STARTING_AGENT或ACTIVE后,设备按创建响应中的heartbeat_interval_sec调用心跳接口。请求至少包含严格递增的sequence_no、设备时间与单调时钟、云端通话状态、RTC上下行状态、网络、固件版本、已应用配置版本、录音队列以及命令结果数组。录音队列必须报告待上传数和失败数;发生失败时可附带脱敏的last_failure_code与last_failure_at,家庭端据此显示上传积压和最近失败原因。

PostgreSQL保存last_heartbeat_seq和权威lease_expires_at;Redis只保存可重建的热租约和设备在线状态。处理规则如下:

  1. 新序号在数据库行锁内更新通话、RTC任务和设备影子,并把租约续期为五个心跳周期;
  2. 同序号重放或更小序号的乱序心跳返回当前结果,不更新设备报告,也不延长数据库或Redis租约;
  3. Redis使用Lua比较序号,避免两个已经提交的并发请求以相反顺序覆盖热租约;
  4. 设备报告的配置版本低于期望版本时,服务端接受心跳并返回最新desired_config_revision;高于云端期望版本时返回冲突,防止未知配置污染设备影子;
  5. RTC已经离房、设备报告正在停止或通话已是终态时返回stop动作,终态永远不会因迟到心跳回到运行态;
  6. 已在创建通话时明确声明rtc_credential_renewal=true的设备,把当前使用的Token过期时间原样放入rtc_credential_expires_at;进入续签窗口时响应在continue后追加renew_rtc_credential,其中rtc结构包含同一房间和设备用户的新短期凭证;未声明能力的旧版v2设备只收到原有动作;
  7. 设备先替换运行内存中的RTC凭证,再在下一次心跳报告新的过期时间。若续签响应丢失,旧过期时间会使服务端按已保存的新过期时间重新生成同一确定性凭证,不重复延长有效期;
  8. Token签名失败时数据库事务整体回滚,不推进心跳序号或凭证时间,设备可用同一序号安全重试;凭证正文仍不写入PostgreSQL、Redis、审计或日志;
  9. PostgreSQL事务提交后Redis刷新失败不回滚权威心跳,后台可从数据库重建热租约。

心跳中的last_error只接受短错误码,不接受自由文本;云端不会把命令结果、短期Token或供应商密钥写入请求日志。

8. 停止通话与异常回收

设备挂断后调用POST /device/v2/conversations/{id}/stop,同时提供16—128字符的Idempotency-Key。正文包含原因代码、设备时间、最后心跳序号、RTC是否已经离房和near/far本地录音分片计数。首次有效请求执行以下事务:

  1. ACTIVE或STARTING_AGENT进入STOPPING,清空权威租约并记录停止原因;
  2. RTC任务进入STOPPING;
  3. 创建唯一的conversation.agent_stop_requested可靠待办和操作审计;
  4. 事务提交后删除Redis通话热租约,并把设备在线状态恢复为idle。

Worker始终使用创建通话时固定的provider_task_ref调用Stop。相同幂等键重放返回原结果;换新幂等键重复停止、Worker重试或后台对账也不会产生第二个供应商任务。供应商返回COMPLETED或签名结束回调到达后,通话和RTC任务统一进入COMPLETED、写入结束时间,并追加STOPPING → COMPLETED状态历史。完全相同的结束回调只落一条供应商事件。

家庭用户在通话中撤回该通话冻结快照正在使用的字幕、原录音、故事整理或个性化记忆授权时,SaaS在撤回事务内同步保存停止意图:ACTIVE/STARTING_AGENT进入STOPPING并创建唯一Agent停止待办,尚未启动Agent的房间直接进入EXPIRED。设备不需要新增接口;下一次通话心跳返回stop,批量遥测若引用该通话则返回stop_conversation。即使Worker先完成供应商停止,设备对已进入终态的通话心跳仍收到停止动作。家庭分享授权只控制后续分享访问,不中断实时语音。

如果设备在激活前挂断,ROOM_ISSUED直接进入EXPIRED,供应商侧不会发生Start或Stop调用。后台恢复扫描器从PostgreSQL扫描非终态通话并使用数据库行锁和SKIP LOCKED避免多个Worker重复修复:

  • 房间预留到期:进入EXPIRED;
  • ACTIVE租约到期:进入STOPPING并可靠投递Stop;
  • STARTING_AGENT长时间未收敛:按时间桶投递幂等启动对账;
  • STOPPING长时间未完成:按时间桶安全重试Stop;
  • 供应商幂等Start/Stop响应已经确认终态时,即使原回调丢失也可由Worker补齐状态历史;
  • 每次发现和恢复均写入conversation.recovery系统审计。

本地默认每5秒扫描一次,启动中或停止中超过20秒进入对账。生产值必须依据真实供应商回调延迟、设备心跳间隔和压测结果调整。

9. 终稿补传与冲突处理

当供应商最终转写缺失或不完整时,通话所属设备可调用POST /device/v2/conversations/{id}/turns:batch补传1—500条最终话轮。请求同时携带设备签名和16—128字符的Idempotency-Key。每条终稿包含turn_key、严格正整数sequence_no、说话方、起止时间、最终文本和是否被打断;同一批内turn_key和sequence_no分别唯一。

云端在单个数据库事务内按通话归属、留存策略和当前文字留存授权校验后,以turn_key和sequence_no同时合并:

  1. 两个业务键、说话方、时间、文本和打断标志完全一致时计入duplicate_count,不重复保存;
  2. 两个业务键均未使用时以source_type=device保存并计入accepted_count;
  3. 任一业务键已经对应不同终稿时返回409 TRANSCRIPT_TURN_CONFLICT,冲突项位于error.details.conflicts,整批新增全部回滚;
  4. 留存模式为none或文字留存授权已经撤回/过期时返回403 TRANSCRIPT_STORAGE_NOT_ALLOWED;
  5. 至少新增一条终稿时可靠投递内容再提取任务;相同幂等请求只返回原合并结果,不重复生成任务。

终端收到409后不得覆盖云端终稿,应记录冲突并拉取云端通话内容供排查;网络失败或503时保持原正文和原幂等键退避重试,但必须为每次HTTP请求生成新的签名随机数。

10. 批量遥测、错误与命令结果

设备在通话中或空闲期均可调用POST /device/v2/telemetry:batch,每批提交1—500条按设备单调编号的事件。事件类型分为status、error和command_result,每条事件必须提供设备内唯一的event_key、sequence_no和发生时间,并且只携带与event_type对应的同名对象。请求需使用设备签名和16—128字符Idempotency-Key。

  • status更新设备实际固件、实际配置版本、网络、RTC和录音队列快照;旧序号不能覆盖新快照,报告版本高于云端期望版本返回409。
  • error只接受最长80字符的有限错误码和warning/error/critical级别,不接收可能包含个人信息的自由文本堆栈。
  • command_result把属于当前设备且未过期的命令收敛到ACKNOWLEDGED/SUCCEEDED/FAILED,并同步固件分配进度;结果对象递归拒绝密钥、令牌、口令、私钥和凭据字段。
  • conversation_id可选,但一旦提交,云端必须验证通话属于当前签名设备;终态或停止中的通话返回stop_conversation动作,不能借遥测访问任意长者的通话。
  • 完全相同的事件计入duplicate_count;同一事件键或序号对应不同内容返回409 TELEMETRY_EVENT_CONFLICT并回滚整批。响应还可返回refresh_config或pull_commands动作。

当前命令结果字段是本地联调基线,硬件团队给出最终设备能力表和结果格式后,应先更新OpenAPI兼容说明再调整实现;不能依赖固件User-Agent猜测字段版本。

11. 运行模拟设备与端到端冒烟

完整环境启动后执行:

Set-Location E:\Codex\story\platform
docker compose --profile simulation run --rm sim-device

模拟器默认执行bootstrap和配置读取,只打印状态与脱敏编号,不打印签名密钥。默认设备公开编号为10000000-0000-0000-0000-000000000511;可通过SIM_API_URL、SIM_DEVICE_ID和SIM_DEVICE_SECRET覆盖本地值。

SIM_SCENARIO支持逗号分隔的场景:

场景 行为
bootstrap 启动、时间同步和版本化配置读取
offline 先向不可达端点发起请求,确认本地保留后使用新Nonce重连
conversation 创建、创建重放、激活并轮询到ACTIVE、心跳、停止和停止重放
transcript 对本次或SIM_CONVERSATION_ID指定通话补交最终字幕并验证幂等重放
telemetry 批量上报设备状态,可关联本次通话,并验证幂等重放
commands 拉取当前设备未终结命令;普通命令提交合成成功结果,firmware_update执行私有下载、SHA-256、Ed25519签名、安装和确认状态链
media 为SIM_CONVERSATION_ID指定且具有当前录音授权的通话生成或读取WAV PCM16文件,直传对象存储,轮询至云端校验完成,并在local_delete_after到达后回报本地密文已删除;可设置SIM_MEDIA_FILE、SIM_MEDIA_DURATION_MS、SIM_MEDIA_TRACK、SIM_MEDIA_VERIFY_TIMEOUT和SIM_MEDIA_DELETE_WAIT_MAX
all 依次执行启动、完整通话、字幕、遥测和命令;录音因需要单独授权和可访问的对象存储公开地址而不自动包含

Windows真实RTC模拟设备会读取创建通话响应中的privacy.record_near、record_far和upload_allowed。只有相应授权有效时,它才通过RTC SDK音频帧回调分别采集长者近端和智能体远端PCM;结束通话时下混为单声道WAV PCM16,按不超过9分钟且不超过64MiB自动分片。设备提交停止请求并等待通话进入COMPLETED或FAILED后,自动创建录音上传批次、直传短期对象地址并轮询至云端校验完成。未授权轨道不会采集,上传失败最多自动重试三次并在模拟器事件流中显示稳定错误。

例如执行联合模拟闭环:

docker compose --profile simulation run --rm -e SIM_SCENARIO=offline,conversation,transcript,telemetry,commands sim-device

固件执行器不会信任命令下发通道携带的公钥。运行前必须通过SIM_FIRMWARE_TRUSTED_KEYS预置本地信任锚,值为“密钥编号到标准Base64 Ed25519公钥”的JSON对象,例如{"vendor-2026":"<32字节公钥的Base64>"}。执行器只接受固定的/device/v2/firmware-releases/{id}/download相对路径,拒绝任意公网地址、查询参数、片段和路径穿越;下载对象限制为1字节至256 MiB。下载后先校验小写SHA-256,再使用本机型号、硬件版本、实际字节数和命令清单重建firmware-manifest-v1并验证签名,全部通过才进入安装阶段。SIM_DEVICE_MODEL和SIM_DEVICE_HARDWARE_REVISION默认分别为simulated-terminal和sim-rev-a,必须与制品签名清单一致。

SIM_FIRMWARE_FAULT可设置为download_failed、digest_mismatch、signature_mismatch、install_failed、interrupt_after_download或interrupt_after_install;默认none。前三类安全失败和安装失败都会提交明确的FAILED终态;两类中断保留ACKNOWLEDGED阶段,设备进程重新启动后会重新拉取同一未终结命令并从安全起点重复执行,不把状态回退为DELIVERED。SIM_FIRMWARE_STEP_DELAY可在0至30秒内模拟下载校验后及安装后的耗时。

启用SIM_FIRMWARE_AB_UPDATE=true后,模拟器把制品写入非活动分区,使用SIM_FIRMWARE_STATE_FILE持久化启动尝试,并上报staging、reboot_pending和boot_verifying细分阶段。启动健康失败或超过SIM_FIRMWARE_BOOT_HEALTH_WINDOW会恢复旧分区并以FAILED/rolled_back报告已执行回退;power_loss_after_stage、power_loss_during_boot和boot_health_failed只在A/B模式下允许。完整状态、字段、顺序和真机边界见设备A/B分区固件升级与启动确认协议。

轻量纵向验收执行:

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke-sim-firmware.ps1

脚本只建立一个临时公钥、一个签名版本、一个单设备批次和一条命令,先在制品校验完成后模拟进程中断,再以同一设备重新运行并核对SUCCEEDED/CONFIRMED/100及影响汇总,最后精确清理数据库和对象存储夹具。若目标设备已有未终结命令,脚本会在创建夹具前停止,避免消费真实在途任务。

仅验证A/B启动失败自动回退时执行-Mode ABRollback。该模式只增加一个本地状态文件,不增加固件版本或目标设备数量;验证完成后同样精确清理。

本地实测组合场景已收敛为COMPLETED|COMPLETED,供应商Start/Stop各1次、3条最终话轮(2条供应商+1条设备补交)、1条关联遥测且无开放Outbox。录音场景可在宿主机使用go run ./cmd/sim-device执行,以便localhost:59000短期上传地址可达;模拟器会等待校验结果并自动提交删除回执。Compose本地环境把MEDIA_LOCAL_DELETE_GRACE设为1秒,生产默认值仍为1小时。若删除时间超过SIM_MEDIA_DELETE_WAIT_MAX,模拟器明确打印计划时间并安全退出,不会提前回报。SIM_MEDIA_FILE仅作为操作人员提供的输入夹具,模拟器只释放内存副本,绝不删除该源文件。

完整环境启动后,可执行自动化通话冒烟:

.\scripts\smoke-conversation.ps1

脚本会验证创建、首次激活、相同幂等键重放、Worker调用、两次签名回调去重和最终ACTIVE状态,并断言供应商仅启动一次。脚本只使用合成数据,验证结束后按本次通话编号精确清理数据库痕迹。

12. 错误处理

HTTP状态 错误码 处理建议
400 VALIDATION_FAILED 修正字段后重新生成随机数和签名
401 DEVICE_AUTHENTICATION_FAILED 检查设备登记、时钟、摘要、签名或是否发生重放
503 DEVICE_AUTHENTICATION_UNAVAILABLE 退避后使用新随机数重试,不关闭鉴权
503 DEVICE_CONFIG_UNAVAILABLE / DEVICE_BOOTSTRAP_FAILED 退避重试并保留上一次有效配置
503 RUNTIME_CONFIG_UNAVAILABLE 生产环境没有命中活动语音路由;停止创建通话,由运营发布或恢复目标范围配置
403 CONSENT_REQUIRED 不重试,等待有效授权
403 TRANSCRIPT_STORAGE_NOT_ALLOWED 停止补传,等待留存策略或授权恢复,不得本地绕过
409 DEVICE_NOT_BOUND / CONVERSATION_ALREADY_ACTIVE 修复绑定或查询已有通话
409 IDEMPOTENCY_KEY_REUSED 不得用同一幂等键提交不同正文
409 ROOM_ALLOCATION_EXPIRED 重新创建通话并取得新的房间预留
409 RTC_ALLOCATION_MISMATCH 丢弃本地房间资料,重新拉取或重建通话
409 CONFIG_VERSION_CONFLICT 先重新获取设备配置再尝试创建新通话
409 PROVIDER_EVENT_CONFLICT 供应商事件编号复用且正文不同,转人工排查
409 CONVERSATION_STATE_CONFLICT 查询云端当前通话状态,不要强制覆盖状态机
409 TRANSCRIPT_TURN_CONFLICT 保留本地终稿和冲突明细,禁止覆盖云端终稿,转入人工对账
409 TELEMETRY_EVENT_CONFLICT 保留冲突事件并停止推进本地确认点,人工核对事件键和单调序号
404 TELEMETRY_CONVERSATION_NOT_FOUND 删除错误通话引用,不得改用任意长者编号重试
409 DEVICE_COMMAND_STATE_CONFLICT 重新拉取命令;不得覆盖已过期、已取消或其他设备的命令
503 TRANSCRIPT_BATCH_UNAVAILABLE 使用原幂等键和原正文退避重试,并生成新的设备签名随机数
503 DEVICE_TELEMETRY_UNAVAILABLE 保留本地事件队列,以原正文和原幂等键退避重试
503 CONVERSATION_HEARTBEAT_UNAVAILABLE / CONVERSATION_STOP_UNAVAILABLE 使用新随机数退避重试;停止请求保留原幂等键和原正文

服务端故意不区分“设备不存在”“设备已吊销”和“签名错误”的外部提示,避免泄露设备登记状态;内部可通过请求关联编号排障。