移动数据面协议(M3)

September 2, 2026 · View on GitHub

手机信封:{ id, method, params? }{ id, ok: true, result }{ id, ok: false, error: { code, message } }。 表外方法一律 forbidden。写操作失败(上游 result.ok === false 或抛错)折成 upstream_error,只回传上游 error.code / error.message,不回传 details 或 prompt。

白名单

方法paramsresult
status.get(可省略){ protocolVersion, minCompatibleMobileVersion, pluginVersion, dshVersion, deviceScope }
session.list{}{ items: [{ sessionId, title?, running, blank, updatedAt, cwd? }] }
session.history{ sessionId, beforeSeq?, maxMessages? }{ events, hasMore }(去掉巨大 attachment data)
session.subscribe{ sessionId }{ accepted: true },开始接收该会话 mux 推送
session.unsubscribe{ sessionId }{ accepted: true }
host.subscribe{}{ accepted: true },开始接收 host 推送
session.prompt{ sessionId, mode?: 'queue'|'steer', text }content:[{ type:'text', text }],默认 queue;空 text → invalid_params
session.cancel{ sessionId }上游 accepted
session.create{ cwd? }上游 sessions.create;审计只记 method + cwd basename
respond见下{ accepted: true }
device.name{ name }{ accepted: true };仅作为认证身份字段未被保存时的兼容回退

session.prompt / session.cancel / respond 记审计 rpc_writedetail 只有 { method, sessionId }

数据面 POST /m/claim { code }(8 位配对 PIN)→ { offer },供手机手输;失败不计完整码。每 IP 每分钟最多 8 次失败。

推送信封

已认证连接上的服务端推送:

{ push, data, rpcId? }

pushsession.event | session.subscribed | approval.requested | approval.resolved | question.requested | question.resolved | session.queue | host.event

approval.requested / question.requested 必带 rpcId(mux 帧外层原样保留),供 respond 回显。

mux 重连后手机需再 session.subscribe / host.subscribesession.queue 的 items 只含 id / placement / 文本摘要。

可选离线推送桥(管理面配置,默认关闭):approval.requested 时可向 allowlist 内的 ntfy/Bark endpoint 发送脱敏提醒。深链格式:/m/?focus=approval&sessionId=…&approvalId=…(手机页打开后落到该会话审批卡)。

respond 两种 payload

审批:

{ rpcId, sessionId, approvalId, outcome: 'allowed-once' | 'rejected' }

提问:

{ rpcId, sessionId, answers: [{ id, selected, custom? }] }

rpcIdsessionIdinvalid_params

认证身份扩展

e2ee_auth 保留原有四个必填字段,并允许两个可选字段:

{
  "type": "e2ee_auth",
  "v": 1,
  "transcriptHashB64": "…",
  "deviceToken": "…",
  "deviceName": "Pocket DSH",
  "clientMetadata": {
    "mobileProtocolVersion": 1,
    "locale": "zh-CN",
    "platform": "Android"
  }
}
  • 服务端接受可选 deviceName / clientMetadata,旧客户端完全不发送这些字段时仍按原四字段形状认证。
  • 官方移动端默认继续发送冻结的四字段认证消息,认证成功后再通过可选 device.name RPC 设置名称,因此新移动端仍能连接严格校验旧形状的桌面端;握手扩展保留给显式协商后的客户端。
  • deviceName 进行 Unicode NFC、首尾去空白和连续空白折叠;控制字符与格式控制字符会使认证失败。协议不自行声明无依据的长度上限。
  • clientMetadata.platform 可省略;元数据只用于兼容诊断,不进入握手 transcript,也不作为授权依据。
  • 新客户端在认证后仍可调用 device.name,用于对尚未消费身份扩展的兼容实现补写名称;它不是新协议的主命名路径。

会合中继外层(dshmr-relay/v1

可选通道。Worker 只转发拼接后的字节;改上面的 RPC 表,也dshmr-e2ee/v1HANDSHAKE_CONTEXT.transport 仍为 direct。Offer 仍是 v1 exact keys,中继只写进 pageUrl / endpoint

控制面(桌面 ↔ Worker,JSON text):

消息方向字段
host_hello桌面 → Workerv, hostId, hostToken
host_ok / host_errorWorker → 桌面hostId / error.code
invite_put桌面 → Workerinvite, expiresAt, offerId
invite_ackWorker → 桌面offerId
claimWorker → 桌面requestId, code(8 位 PIN,不是完整码)
claim_result桌面 → WorkerrequestId, offer?, error?
phone_waitingWorker → 桌面ticket, expiresAt
ping / pong双向

手机:

  • 配对 wss://<origin>/v1/phone/<hostId>?invite=…
  • 重连 wss://<origin>/v1/phone/<hostId>?resume=1

桌面再出站 wss://<origin>/v1/accept/<ticket>,之后与 /m/ws 同一套 E2EE + RPC。规格见 05-cloud-relay.md