qiwei-relay-mode-guide.md 12 KB


title: 企微 Webhook Relay 模式完整配置指南 updated: 2026-07-27 project: claude-code-qiwe-assistant

mode: relay

企微 Webhook Relay 模式完整配置指南

本文档说明如何在 claude-code-qiwe-assistant(MCP 技能包)中使用中央 Relay 模式接收企微实时事件回调。该模式适合 Skill 运行在本地电脑、内网或无固定公网 IP 的场景。

一、Relay 模式是什么

中央 Relay 模式是 Fmode 提供的一种 webhook 中转方案:

  • 企微平台把事件推送到 Fmode 的中央 Relay 服务器(有固定公网地址)。
  • 本地运行的 Skill 通过长轮询主动从 Relay 取回属于自己的事件。
  • 事件在 Relay 端经过 RSA 公钥加密,本地用私钥解密后再处理。

与公网直收的区别

维度 中央 Relay 公网直收
是否需要公网地址 不需要 需要
Skill 网络要求 能访问公网即可 能被公网访问
事件到达方式 本地主动长轮询取回 企微平台被动推送
部署位置 本地电脑、内网、云服务器均可 必须有公网 IP/域名
安全性 RSA 加密 + Tenant Secret 认证 HMAC/Authorization 校验
实时性 秒级延迟(受轮询间隔影响) 实时
适用场景 开发调试、本地运行、无公网 IP 生产服务器、追求低延迟

架构图

┌─────────────┐      HTTP POST      ┌──────────────────┐
│  企微平台    │ ──────────────────▶ │  Fmode 中央 Relay │
└─────────────┘                     │  (公网服务器)      │
                                    └────────┬─────────┘
                                             │
                              长轮询 /api/relay/poll
                              RSA 私钥解密
                              Tenant Secret 认证
                                             │
                                             ▼
                                    ┌──────────────────┐
                                    │ 本地 Skill       │
                                    │ claude-code-    │
                                    │ qiwei-assistant │
                                    └──────────────────┘

二、前置条件

  1. 已安装并配置好 claude-code-qiwe-assistant
  2. 已获取 Fmode 鉴权 token(QIWEI_AUTH_TOKENFMODE_API_KEY)。
  3. 已完成企微设备登录(已有 guid)。
  4. 已从 Fmode 提供方申请到 Relay 租户凭证:
    • TENANT_API_KEY
    • TENANT_API_SECRET
    • RELAY_PRIVATE_KEY(RSA 私钥)
    • RELAY_BASE_URL(中央 Relay 公网地址)

三、获取 Relay 租户凭证

Relay 凭证需要向 Fmode 提供方或你的服务管理员申请。申请时通常需要提供:

  • 你的 Fmode 账号/公司标识
  • 预计接入的设备数量(guid 数量)
  • 是否需要多个 Skill 实例共享同一个租户

申请成功后,你会拿到以下信息:

RELAY_BASE_URL=http://8.138.37.248:4000
TENANT_API_KEY=qk_xxxxxxxxxxxxxxxxxxxxxxxx
TENANT_API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
RELAY_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
...
-----END PRIVATE KEY-----

安全提醒TENANT_API_SECRETRELAY_PRIVATE_KEY 是敏感信息,不要截图传播、不要提交到 Git。

四、目标项目配置

4.1 配置方式

目标项目使用文件存储配置,所有 Relay 配置写入:

outputs/webhook/relay-config.json

你也可以通过 MCP 工具 qiwei_relay_save_config 写入。

4.2 通过工具配置(推荐)

在 Claude Code 中调用:

{
  "name": "qiwei_relay_save_config",
  "arguments": {
    "relayBaseUrl": "http://8.138.37.248:4000",
    "tenantId": "你的租户ID(可选)",
    "publicKey": "对应的 RSA 公钥(可选,用于本地调试)"
  }
}

注意:当前 qiwei_relay_save_config 工具只保存 relayBaseUrltenantIdpublicKey不保存 tenantApiKeytenantApiSecretprivateKey。为了安全,后三者建议写入 .env.local 或环境变量。

4.3 通过 .env.local 配置(推荐)

在目标项目根目录创建或编辑 .env.local

# Fmode 鉴权 token(已有)
QIWEI_AUTH_TOKEN=sk-xxxxxxxx

# Relay 中央服务器地址
RELAY_BASE_URL=http://8.138.37.248:4000

# Relay 租户凭证
TENANT_API_KEY=qk_xxxxxxxx
TENANT_API_SECRET=xxxxxxxx

# RSA 私钥,必须写成单行,用 \n 替换真实换行符
RELAY_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n-----END PRIVATE KEY-----

私钥格式说明.env 文件中不能包含真实换行符,必须将 PEM 私钥中的每一行换行替换为 \n 字面量。程序读取时会自动还原为真实换行符(与源 Qiwei 项目 lib/relay-config.tsgetRelayPrivateKey() 逻辑一致)。

4.4 配置文件示例

outputs/webhook/relay-config.json

{
  "relayBaseUrl": "http://8.138.37.248:4000",
  "tenantId": "tenant_xxx",
  "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----",
  "updatedAt": "2026-07-16T08:00:00.000Z"
}

五、Relay 客户端实现

5.1 当前目标项目缺失的部分

目标项目已有:

  • mcp/src/core/webhook-server.js:本地 webhook server(仅落盘)
  • mcp/src/tools/qiwei-webhook-relay-run.js:配置读写工具

缺少真正的 Relay 长轮询客户端。需要从源 Qiwei 项目迁移 lib/relay-client.tsmcp/src/core/relay-client.js

5.2 需要新增的 relay-client.js

核心职责:

  1. .env.local / relay-config.json 读取 Relay 凭证。
  2. 获取本地可用设备 guid
  3. 长轮询 POST {RELAY_BASE_URL}/api/relay/poll
  4. 用 RSA 私钥解密 encryptedPayload
  5. 把解密后的事件喂给本地 webhook 处理逻辑。
  6. ACK 已处理事件:POST {RELAY_BASE_URL}/api/relay/ack
  7. 失败时指数退避重连。

参考实现要点(来自源项目 lib/relay-client.ts):

const POLL_WAIT_MS = 30000;
const INITIAL_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 60000;

async function runPollOnce() {
  const response = await fetch(`${RELAY_BASE_URL}/api/relay/poll`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${TENANT_API_SECRET}`
    },
    body: JSON.stringify({ guid: deviceGuid, batchSize: 100, waitMs: POLL_WAIT_MS })
  });

  const data = await response.json();
  for (const event of data.events) {
    const decrypted = decryptPayload(event.encryptedPayload, RELAY_PRIVATE_KEY);
    const payload = JSON.parse(decrypted);
    await processWebhookEvents({ code: 0, msg: 'from-relay', data: [payload] });
  }

  await ackEvents(deviceGuid, data.events.map(e => e.eventId));
}

function decryptPayload(encryptedPayload, privateKey) {
  const key = crypto.createPrivateKey(privateKey);
  const buffer = Buffer.from(encryptedPayload, 'base64');
  const decrypted = crypto.privateDecrypt({ key, oaepHash: 'sha256' }, buffer);
  return decrypted.toString('utf8');
}

5.3 私钥还原

读取 .env.local 中的私钥时,必须做换行还原:

function getRelayPrivateKey() {
  return (process.env.RELAY_PRIVATE_KEY || '').replace(/\\n/g, '\n');
}

这是源 Qiwei 项目 lib/relay-config.ts 中的关键处理,迁移时必须保留。

六、启动 Relay 客户端

6.1 启动方式选择

目标项目是 stdio MCP server,不建议在主进程内启动长轮询,否则会阻塞 MCP 消息循环。推荐以下方式:

方案 A:独立子进程(推荐)

新增 scripts/start-relay-client.js

node scripts/start-relay-client.js

该脚本单独运行,与 MCP server 解耦。

方案 B:Dashboard 进程内启动

如果已使用 npm run dashboard 启动 dashboard,可在 dashboard server 启动时附带启动 Relay 客户端。

方案 C:MCP 工具触发(不推荐长期运行)

通过 qiwei_relay_connect 工具 fork 子进程启动。这种方式会话结束后可能随 MCP server 一起退出,不够稳定。

6.2 推荐启动流程

# 1. 确保 .env.local 已配置 Relay 凭证
# 2. 启动 MCP server(正常对话即可)
# 3. 在另一个终端启动 Relay 客户端
node scripts/start-relay-client.js

或封装为 npm script:

{
  "scripts": {
    "relay": "node scripts/start-relay-client.js"
  }
}

七、验证 Relay 是否正常工作

7.1 检查配置

调用 MCP 工具:

{ "name": "qiwei_relay_config" }

应返回:

{
  "status": "ok",
  "data": {
    "configured": true,
    "relayBaseUrl": "http://8.138.37.248:4000"
  }
}

7.2 检查 Relay 客户端日志

启动 scripts/start-relay-client.js 后,观察日志:

[RelayClient] 启动 Relay 轮询
[RelayClient] 取回 3 条事件
[RelayClient] ACK 3 条事件

7.3 触发真实事件

让好友通过你的企微账号,或在客户群里发送一条消息。观察:

  • outputs/webhook/events/ 目录下是否生成新事件文件
  • outputs/messages/<roomId>/ 是否出现新消息文件
  • 画像文件 outputs/portraits/<externalUserId>.json 是否被触发更新

7.4 常见问题排查

现象 可能原因 排查方法
轮询无事件 企微回调未配置到 Relay 检查 Fmode 平台设置的回调地址是否为 Relay 的 ingest URL
解密失败 私钥格式错误 确认 .env.local 中私钥使用 \n 单行存储,程序正确还原
401/403 Tenant Secret 错误 检查 TENANT_API_SECRET 是否与 Relay 端匹配
获取不到 guid 设备未登录 先调用 qiwei_login_start 完成扫码登录
事件处理报错 webhook 处理逻辑未迁移 检查 processWebhookEvents() 是否正常

八、Relay 回调地址说明

当 Relay 模式启用后,企微平台侧的回调地址应配置为:

{RELAY_BASE_URL}/api/webhook/ingest

企微回调对服务 Token 全局生效。Skill 先把真实设备 guid 注册到 Relay,再调用 Fmode 专用接口 POST /relay/connect;由 Fmode 服务端持有回调密钥并调用 /client/setCallback。客户端不能提交回调 URL 或签名密钥。

旧的 /{tenantId}/{guid} 入口仅保留兼容,不用于新部署。

九、安全注意事项

  1. 不要把 TENANT_API_SECRETRELAY_PRIVATE_KEY 提交到 Git。目标项目 outputs/ 已在 .gitignore 中,但 .env.local 需要自行确认是否忽略。
  2. 私钥单行存储时使用 \n 字面量,不要直接粘贴带真实换行的 PEM。
  3. 定期轮换密钥。如果怀疑凭证泄露,立即联系 Fmode 提供方重置。
  4. ACK 所有事件,包括解密失败的,避免 Relay 端重复投递导致死循环。
  5. 本地 webhook server 签名验证仍可保留。即使事件来自 Relay,本地处理前也可以再做一层校验。

十、与源 Qiwei 项目的差异

源 Qiwei 项目 目标 MCP 项目
lib/relay-client.ts 需新增 mcp/src/core/relay-client.js
lib/relay-config.ts 复用 mcp/src/core/webhook-server.js 的配置读写,加 .env.local 读取
lib/webhook-setup.ts 合并到 mcp/src/tools/qiwei-webhook-relay-run.js
SQLite WebhookEvent outputs/webhook/events/ 文件
processWebhookEvents()api/module/webhook/routes.ts 迁移到 mcp/src/core/webhook-processor.js
启动时自动启动 Relay 客户端 改为独立进程 scripts/start-relay-client.js

十一、下一步建议

  1. 在目标项目创建 mcp/src/core/relay-client.js(从 Qiwei lib/relay-client.ts 迁移)。
  2. 创建 scripts/start-relay-client.js 作为独立启动入口。
  3. 增强 qiwei_relay_connect 工具,支持一键启动 Relay 客户端。
  4. processWebhookEvents() 和事件解析逻辑迁移到 mcp/src/core/webhook-processor.js
  5. 写完后按第 7 节验证清单测试。

需要我继续执行实际的代码迁移吗?