Skip to content

钉钉桥接服务(Node 自建服务器版)

目标:钉钉事件回调 → Node/Express 服务(验签/防重放/格式化)→ OpenClaw Gateway /hooks/agent

适合:

  • 你需要更复杂的逻辑(媒体、队列、长任务、持久存储)
  • 你希望把“把 OpenClaw 回复发回钉钉”也做成完整闭环

0) 前置:OpenClaw 开启 hooks

json5
{
  hooks: {
    enabled: true,
    token: "<OPENCLAW_HOOKS_TOKEN>",
    path: "/hooks"
  }
}

假设可访问地址:

  • http://127.0.0.1:18789/hooks/agent(同机)
  • https://claw-api.example.com/hooks/agent(反代后)

1) Node 服务结构(建议)

  • server.js:HTTP server + 路由
  • verify.js:钉钉验签 + 防重放
  • normalize.js:把钉钉 payload 归一化为文本

环境变量:

  • OPENCLAW_HOOKS_URL
  • OPENCLAW_HOOKS_TOKEN
  • DINGTALK_SIGNING_SECRET

2) Express 示例代码(骨架)

js
import express from 'express';

const app = express();
app.use(express.text({ type: '*/*' }));

function verifyDingTalk(req, rawBody) {
  // TODO: 按钉钉官方文档实现验签
  // 建议:校验 timestamp + nonce + signature,并做 nonce TTL 防重放(Redis/内存LRU/SQLite)
  return true;
}

function normalizeToText(payload) {
  const text = payload?.text?.content ?? payload?.content ?? JSON.stringify(payload);
  return `【钉钉】${text}`;
}

app.post('/dingtalk/events', async (req, res) => {
  const rawBody = req.body || '';

  if (!verifyDingTalk(req, rawBody)) {
    return res.status(401).json({ ok: false, error: 'Bad signature' });
  }

  const payload = rawBody ? JSON.parse(rawBody) : {};

  // TODO: 钉钉握手/校验 challenge(若协议要求)

  const message = normalizeToText(payload);

  const r = await fetch(process.env.OPENCLAW_HOOKS_URL, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'authorization': `Bearer ${process.env.OPENCLAW_HOOKS_TOKEN}`,
    },
    body: JSON.stringify({
      name: 'DingTalk',
      message,
      wakeMode: 'now',
      deliver: true,
      channel: 'last',
    }),
  });

  return res.json({ ok: true, forwarded: r.ok });
});

app.listen(8787, () => {
  console.log('listening on http://127.0.0.1:8787');
});

启动:

bash
OPENCLAW_HOOKS_URL="http://127.0.0.1:18789/hooks/agent" \
OPENCLAW_HOOKS_TOKEN="xxx" \
DINGTALK_SIGNING_SECRET="yyy" \
node server.js

3) 反代与 HTTPS(必做)

钉钉回调通常要求 HTTPS 公网可达:

  • 你可以用 Nginx/Caddy 做反代 + TLS
  • 或 Cloudflare Tunnel 把 /dingtalk/events 暴露出来

4) 双向回传(把 OpenClaw 回复发回钉钉)

要在钉钉里形成“完整对话”,Node 服务需要:

  • 从 OpenClaw 获取最终输出(可通过:让 OpenClaw webhook 回调你的 Node,或你自己实现 channel plugin)
  • 调用钉钉发送消息 API(机器人/应用接口)

最干净的长期方案仍是:写一个钉钉 channel plugin,把入站/出站都封装在 OpenClaw 内。


5) 安全清单

  • 验签 + 防重放
  • 限流(按 tenant/chat)
  • 记录 requestId 与 traceId 便于排错
  • 不把原始 payload 全量写入日志(避免敏感信息泄露)