云手机或云浏览器的操作一旦得到最终结果,MoreLogin 就会向你自己的 HTTPS 地址 POST 一个 JSON 事件,你不必再轮询结果。每个团队只保存一个回调地址,Local API 与 Open API 写的是同一份记录。
两个入口提供同一个操作。Open API:
curl -X POST "https://api.morelogin.com/webhook/config" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"callbackUrl": "https://example.com/morelogin/webhook",
"enabled": true
}'Local API:
curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
-H "Content-Type: application/json" \
-d '{
"callbackUrl": "https://example.com/morelogin/webhook",
"enabled": true
}'Local API 这条路由由桌面客户端提供,因此需要 MoreLogin 客户端 v2.66.0 或更高版本。Open API 入口不依赖客户端。
teamId 不会从请求中读取。每次调用配置的都是服务端上下文所属的团队,因此你无法把其他团队的事件指向自己的地址。
地址必须是 HTTPS、1 到 1024 个字符,带主机名,不含用户信息与片段,端口合法。它解析到的每个地址都必须是公网可路由的:localhost、内网、链路本地、CGNAT、云元数据、协议保留、文档示例与 6to4 等范围会被 41001 拒绝。实际投递建连时会再校验一次,因此某个域名若之后解析到内网范围,只会导致该次投递失败,而不会打到你的内网。
保存是按团队的 upsert,因此同样的请求体发两次没有副作用。
enabled: false 会保留已存的地址并停止创建新事件。已排队的事件会被逐条取消,取消发生在每个事件下次被授权的时刻,且仅当配置那时仍处于停用状态——所以如果你很快重新启用,排队中的事件会继续投递,而不会被丢弃。
每次投递都是带以下请求头的 POST:
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
User-Agent | MoreLogin-Webhook/1.0 |
X-Webhook-Event-Id | 与正文中的 eventId 相同 |
X-Webhook-Event-Type | 与正文中的 eventType 相同 |
这两个 X-Webhook-* 请求头刻意与正文字段重复,这样路由或队列就能直接按它们分发,不必解析 JSON。
{
"eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
"eventType": "cloud_phone.power_off",
"eventTime": "1787241600000",
"data": {
"teamId": "10001",
"cloudPhoneId": "20001",
"reason": "MONEY_SAVING"
},
"result": {
"success": true,
"code": "0",
"message": null
}
}| 字段 | 说明 |
|---|---|
eventId | 唯一事件 ID。同一事件的重试与内部重放都复用它,因此它就是幂等键 |
eventType | 稳定的事件名称,取值见下表 |
eventTime | 最终结果发生的时间,Unix 毫秒时间戳。以字符串发送 |
data.teamId | 拥有该资源的团队。以字符串发送 |
data.cloudPhoneId | 该事件对应的云手机。以字符串发送 |
data.cloudBrowserId | 该事件对应的云浏览器环境。以字符串发送 |
data.runId | 该事件对应的云浏览器运行实例 |
data.reason | 仅在 cloud_phone.power_off 与 cloud_browser.stop 上出现 |
result.success | 该操作最终是否成功 |
result.code | 成功时为 0;否则是与语言无关的稳定业务错误码 |
result.message | 成功时为 null;否则是按团队语言给出的说明,缺失时降级为 en-US |
所有数字型标识和 eventTime 都以 JSON 字符串到达,与 API 其余部分一致——参见 通用响应格式。请按字符串解析而不是按数字,否则 64 位 ID 会丢精度。
eventType | 它报告的最终结果 |
|---|---|
cloud_phone.power_on | 开机成功或失败 |
cloud_phone.power_off | 关机成功。关机失败不产生事件 |
cloud_phone.restart | 重启成功或失败 |
cloud_phone.reset | 重置成功或失败 |
cloud_phone.new_machine | 一键新机成功或失败 |
cloud_browser.start | 云浏览器开启成功或失败 |
cloud_browser.stop | 云浏览器每一次关闭尝试,无论成功与否 |
云手机事件携带 teamId 与 cloudPhoneId。云浏览器事件携带 teamId、cloudBrowserId 与 runId。
开机、重启、重置与一键新机只有在供应商回调成功且本地同步已提交后才报告 success: true。开机计费失败,或异步代理检测失败并完成失败收尾时,都报告开机失败,且不会另外产生关机事件。安装预设应用、清理过期文件等推迟执行的任务不改变该结果。
若关机操作打断了开机过程,本次尝试不产生开机事件;随后真正关机成功时仍会产生独立的 cloud_phone.power_off。
cloud_phone.power_off 会在 data.reason 中给出:
| 取值 | 含义 |
|---|---|
NORMAL | 普通关机 |
MONEY_SAVING | 省钱模式 |
OTHER | 欠费、到期、运维或其他原因 |
cloud_browser.stop 会在 data.reason 中给出:
| 取值 | 含义 |
|---|---|
NORMAL | 普通关闭 |
MONEY_SAVING | 省钱模式 |
ENERGY_SAVING | 节能模式 |
BROWSER_EXITED | 浏览器异常退出 |
OTHER | 其他原因 |
内部归档失败只影响 result,绝不覆盖关闭流程开始时固化的 reason。
任何 2xx 都算投递成功。其他任何状态码,以及连接超时、读取超时和网络错误,都算失败。连接超时为 5 秒,读取超时为 30 秒。
投递失败后最多重试 6 次,间隔依次为 10 秒、30 秒、60 秒、3 分钟、10 分钟和 30 分钟。也就是单个事件最多 7 次请求。第 6 次重试仍失败后,该事件不再自动重试。
同一台云手机、或同一个云浏览器的事件,按创建顺序逐个投递。不同资源之间并行投递,因此不要假设它们之间有任何先后顺序。
同一事件的重试始终复用相同的 eventId、eventTime 和请求正文。在网络或服务恢复的极端场景下,同一事件可能被投递多次且这些值保持不变,因此请按 eventId 去重。
对云浏览器关闭来说,一次关闭尝试恰好产生一个事件。之后的人工重试、人工强制释放,或系统发起的强制释放都属于新操作,会得到新的 eventId。两次尝试绝不会被合并成一个事件。
云手机操作通过供应商任务 ID、traceId 或稳定的业务时间来识别。三者都不存在时,服务会生成一个 UUID 并把当前的最终回调视为一次新操作,因此你会收到新的 eventId,而不会让两次不同的操作被合并成一个。
存下事件后就尽快返回 2xx,真正的处理放到异步。响应正文会被忽略,而处理缓慢会耗掉你 30 秒的读取预算,即使你已经收到了事件,它也会被推入重试队列。
按 eventId 去重。遇到重复请直接视为已处理,而不要重复执行副作用。
不要因为请求打到了你的地址就把正文当作可信输入。本期不对回调签名,因此任何知道你地址的人都能向它发送请求。请使用难以猜测的地址,并且在执行代价较大的动作之前,先通过 API 确认状态——例如云手机用 POST /cloudphone/info,云浏览器运行实例用 POST /cloudbrowser/page。
result.code 使用与同步接口相同的业务错误码,因此失败可以在 错误码 中查到。result.message 是给人看的字符串,措辞与语言都可能变化;请按 result.success 和 result.code 分支,绝不要按 result.message 分支。
每个事件的机器可读契约,包括 schema 与逐事件示例,随共享资源规格一起发布,见 Shared Resources Open API。