关店后有人进门,监控值班群却没响:用 setMessageCallback 订 human,再用 aiHuman 打开人形

0 阅读8分钟

目录


关店后 App 会亮,值班通道为什么是聋的

无人便利店、社区小店,镜头早就挂了。白天店里有人,晚上把乐橙 App 通知打开,以为关店等于布防。第二天翻回放才发现:货架前走过一个人,红点停在私人手机里,值班群是静的。

白天客流本身就会出人形。全天往群里灌,值守的人会关通知;关掉之后,真正该响的夜里也没了。缺的不是再买一台相机,而是把人形检测接到自己的业务通道,并且只在关店后叫醒人。

开放平台这边,人形事件的类型值是 human,落在 alarm 大类里。这篇用现行 OpenAPI 的 setMessageCallback 把告警推到你的 HTTPS,再用 setDeviceCameraStatus 把通道级 aiHuman 打开。

读完按这个顺序交差:

  1. 在开放平台创建应用,把门店设备绑进开发者资产。
  2. 签名自测通过,能拿到 accessToken
  3. listDeviceDetailsByPage 能看到版本号,能力集里有 AiHuman,设备 online
  4. setDeviceCameraStatus 打开 aiHumanmodifyDeviceAlarmStatus 开动检。
  5. setMessageCallbackalarmgetMessageCallback 读回同一条地址。
  6. 人在镜头前走一圈,桥接日志出现 human,值班群进一条。

无人店关店后的闯入:镜头会出 human,App 有红点,值班群没有人。本文用现行 OpenAPI 的 setMessageCallbackalarm,用 setDeviceCameraStatusaiHuman,带可运行 Node.js,验到群里响一条为止。


这篇会用到什么

类型见事件消息类型定义,推送流程见平台主动推送

  1. alarm 才能收到 humanhuman 是人形检测报警,videoMotion 是动态检测,白天客流别和夜里闯入混成同一条规则。
  2. 通道能力集要有 AiHuman,再用 setDeviceCameraStatusenableType 写成 aiHuman(首字母小写)。备注写明:仅支持设备详细信息中 accessType=PaaS 的设备。
  3. 动检总开关仍要开:modifyDeviceAlarmStatusenable=true。人形使能挂在动检之上,只开一边,现场走一圈也没事件。
  4. 回调必须先回 HTTP 200。多次无响应,平台停推,夜里店是聋的。
  5. 一个开发者账号一条 callbackUrl。多店按推送体里的 did 找群,不要每家店起一套服务。
  6. 抓图用 getAlarmMessageByIdpicurlArray。普通报警体里不一定带图,收到要尽快转存。

你负责绑设备和值班群,开放平台负责把 human 推到你的 HTTPS。

门店 IPC
   ├─ 人形智能  →  setDeviceCameraStatus / aiHuman
   └─ 动检开关  →  modifyDeviceAlarmStatus
                         │
                         ▼
              alarm  →  msgType = human
                         │
                         ▼
              你的 HTTPS(立刻 200)
                         │
                         ▼
              按 did 找到门店 → 值班群

为什么轮询、只靠官方 App、一上来写原生都走不通

轮询报警列表适合对账,不适合当值守主路径。扫得再勤也补不上推送那几秒。关店后的三分钟,比第二天回放值钱。

只靠官方 App,红点停在私人手机里。人在睡觉、在别的店、通知被系统折叠,值班群都不知道有人进门。白天客流把通知刷到「关闭」,夜里更没人看。

一上来写原生客户端没必要。值守已经在企微或钉钉,缺的是桥,不是第三个看监控的 App。


动手:从开应用到值班群响第一声

先不要做夜间计划。白天走一圈,群里能进一条 human,再谈关店窗口。

第一步:创建应用,填齐门店台账

打开 乐橙开放平台 注册并创建应用。控制台能看到 appId / appSecret 再往下做。把门店枪机绑进这个开发者资产,不要停在私人 App 号下。准备一个外网能 POST 进来的 HTTPS(联调用穿透,生产用正式证书)。

IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
CALLBACK_URL=https://bridge.example.com/imou/callback
DEVICE_STORE=门店序列号
CHANNEL_ID=0
HOOK_STORE=值班群机器人地址

成功:控制台能看到应用,设备在资产里显示在线。
兜底:设备还在私人 App 下,先做绑定,再谈回调。

第二步:签名壳,先和文档案例对齐

请求走 https://openapi.lechange.cn/openapi/{method},壳子是 system + params + id。签名按开发规范time:{time},nonce:{nonce},appSecret:{appSecret} 拼成原始串,再做 MD5 小写 32 位。time 与服务器误差不能超过 5 分钟,否则 SN1002nonce 5 分钟内不能重复,否则 SN1005

// openapi-client.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';

function calcSign(time, nonce, appSecret) {
  const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
  return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}

async function callOpenApi(method, appId, appSecret, params = {}) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = uuidv4();
  const body = {
    system: { ver: '1.0', appId, time, nonce, sign: calcSign(time, nonce, appSecret) },
    id: uuidv4(),
    params,
  };
  const res = await fetch(`${OPENAPI_BASE}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!json.result || json.result.code !== '0') {
    const msg = json.result ? `${json.result.code} ${json.result.msg}` : JSON.stringify(json);
    throw new Error(`OpenAPI ${method} failed: ${msg}`);
  }
  return json.result.data;
}

module.exports = { callOpenApi, calcSign };

成功:calcSign(1706511734, 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2', 'test123456789test123456789') 得到 fd37b62889e4757c58b8f3bf05fb9976;再调 accessToken 能打印 token,expireTime 单位是秒。
兜底:对不上标准案例,先别查业务接口,多半是字符串拼错。TK1002 再刷 token,不要每个请求都申。管理员 token 有效期 3 天,超过 2 天再申会拿到新的,新旧各自能用。

第三步:列表里先确认这台机能出人形

查设备用 listDeviceDetailsByPage。看四样:deviceStatus 是不是 onlinedeviceVersion 有没有值,accessType 是不是 PaaSdeviceAbility / channelAbility 里有没有 AiHuman。能力开关写法见设备能力开关:列表里是首字母大写,后面使能接口传小写。

token=上一步拿到的 accessToken
page=1
pageSize=10
source=bindAndShare

成功:终端能打印出门店序列号、deviceVersiondeviceStatus=online,能力串里出现 AiHuman
兜底:列表为空,设备绑在私人号下。没有 AiHuman,不要硬开人形,换支持该能力的机型,或先只做动检(videoMotion)——那是另一条规则,这篇不凑合。accessType 不是 PaaS 时,使能接口以现行文档备注为准,不要照抄下一步。

第四步:开人形使能,先让镜头会报

PaaS 设备用 setDeviceCameraStatus 开通道级 aiHuman。动检总开关用 modifyDeviceAlarmStatusenable=true。两个都开,现场走一圈才有机会出 human。货架不会自己逛街,这一步只负责让镜头开口。

# 开人形(通道级)
token=管理员 accessToken
deviceId=门店序列号
channelId=0
enableType=aiHuman
enable=true

# 开动检
token=管理员 accessToken
deviceId=门店序列号
channelId=0
enable=true

成功:两个接口都回 code=0
兜底:使能失败先回头看能力集和 accessType,不要当成签名错了。这一步还不会进群,群响是第六步的事。

第五步:登记回调,只订 alarm

这篇第一闭环必须跑通的接口是 setMessageCallbackstatus=oncallbackUrl 必填,必须公网可达。callbackFlag 先写 alarm,人形在这个大类里。联调 basePush"2",以桥接日志为准,少和 App 推送抢排查。

// set-callback.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');

(async () => {
  const token = (
    await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})
  ).accessToken;

  await callOpenApi('setMessageCallback', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
    token,
    status: 'on',
    callbackUrl: process.env.CALLBACK_URL,
    callbackFlag: 'alarm',
    basePush: '2',
  });

  const current = await callOpenApi(
    'getMessageCallback',
    process.env.IMOU_APP_ID,
    process.env.IMOU_APP_SECRET,
    { token }
  );
  console.log(current);
})();

成功:getMessageCallback 读回的 status=oncallbackUrlcallbackFlag=alarm 与上面填写项一致。
兜底:localhost 登记成功也收不到。先让公网 curl -X POST 打到自己的路径。漏写 alarm,后面走一圈永远安静。

第六步:接收端先回 200,人形进值班群

同步路径零阻塞。文档写明:多次不回 200,平台停推。普通报警体见事件消息格式定义iddidcidmsgTypetime。第一闭环只认 human,白天走一圈也要进群,用来证明链路是通的。

// bridge-server.js
require('dotenv').config();
const express = require('express');

const STORES = {
  [process.env.DEVICE_STORE]: { name: '本店', hook: process.env.HOOK_STORE },
};

const seen = new Set();

function storeOf(did) {
  return STORES[did] || { name: did, hook: process.env.HOOK_STORE };
}

async function toDutyGroup(ev) {
  if (ev.msgType !== 'human') {
    console.log('[忽略]', ev.msgType, ev.did);
    return;
  }
  const key = String(ev.id);
  if (seen.has(key)) return;
  seen.add(key);

  const { name, hook } = storeOf(ev.did);
  const when = new Date(ev.time * 1000).toLocaleString('zh-CN', { hour12: false });
  const text = `【无人店闯入】${name}\nmsgType:${ev.msgType}\n设备:${ev.did} 通道:${ev.cid}\n${when}\n先看门口,再回群`;
  console.log('[值班群]', text, hook ? '→ hook' : '→ 仅日志');
  // 这里按你们现有机器人协议 POST hook,本文不绑具体厂商
}

const app = express();
app.use(express.json({ limit: '1mb' }));

app.post('/imou/callback', (req, res) => {
  res.status(200).json({ code: '0', msg: 'ok' });
  const body = req.body || {};
  const ev = body.msgType ? body : body.data || body;
  setImmediate(() => toDutyGroup(ev).catch((e) => console.error(e)));
});

app.get('/health', (_req, res) => res.status(200).send('ok'));
app.listen(process.env.PORT || 8080);

成功:/health 返回 ok;人在镜头前走一圈,日志出现 human,值班群进一条。这就是第一闭环。
兜底:App 有告警、服务安静,回到第五步看地址和 callbackFlag。日志有 videoMotion 没有 human,回头看第四步 aiHuman 是否真开了。把短信、查库写进同步路径,过不了这一步——先 200,再 setImmediate


群响了之后:夜间窗口和抓图取证

最小闭环已经成立,才加第二层。仍用同一套现行 OpenAPI。

关店后货架过道不该有人,human 这时才值钱。白天客流继续出事件,不要再进值班群。两条线一起做:

设备侧用 modifyDeviceAlarmPlan 把动检计划收成夜间。beginTime / endTime 是当天的 HH:mm:ss,跨午夜拆成两段,每天两行。改完用 deviceAlarmPlan 读回,看每条规则的 enable 和起止时间。

token=管理员 accessToken
deviceId=门店序列号
channelId=0
rules=
  period=Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
  beginTime=22:00:00
  endTime=23:59:59
  period=Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
  beginTime=00:00:00
  endTime=08:00:00

接收侧再加一层营业时间窗,按你们门店的关店钟点滤。设备计划漏配时,群不会被白天客流刷死。时间以推送体里的 time(Unix 秒)换成本地小时。

取证走 getAlarmMessageById。把回调里的 id 当成 alarmId,带上 deviceIdchannelIdmsgType=human。返回里的 picurlArray 尽快落到自己的对象存储。aiTag / aiCopyWriting 只在通道开了智见云存储或 AI 智见套餐时才有值,没有就当普通抓图,不要当接口坏了。

夜里镜头掉线也该叫醒人。第二层再把 callbackFlag 改成 alarm,deviceStatus,认 offline。上下线体的 id 固定是 -1,不能拿去查告警详情。生产把内存 seen 换成 Redis。派单、SLA、多店值班表是下一篇的事,这篇停在「关店后群里有该去的人看到」。


联调会踩的坑

现象多半是什么先做什么
登记成功但一条都没有URL 不是公网 / 证书 / 路径 404curl -X POST 打自己的回调
收过一阵突然没了同步路径堵了,多次没回 200,平台停推修好后重新 setMessageCallback
App 有告警、桥接安静没订 alarm,或设备不在这个开发者账号下getMessageCallback 读回;列表核对 deviceId
只有 videoMotion,没有 human没开 aiHuman,或机型没有 AiHuman查能力集,再跑第四步
使能接口失败accessType 不是 PaaS,或通道号没传列表看 accessTypechannelId
白天群被客流刷爆第一闭环规则原样上了生产上夜间计划,接收侧再滤营业时间
拿上下线 id 去查抓图文档写明固定为 -1只用 humanidgetAlarmMessageById

真正省下的不是多买一台相机

这篇省下的不是再挂一路镜头,而是值守不用第二天翻回放,才在群里问「昨晚谁进过门」。

只做店内大屏预览、不接值班群的,不必在这里把回调搭起来。有人值守、出事了有人盯 App 的柜台店,也没必要上这一套。

appSecret、机器人地址只放环境变量。公网回调建议加自有校验。抓图按门店留存周期转存,不要把顾客人脸长期堆在聊天记录里。接口以现行文档为准。