--- 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 | 生产服务器、追求低延迟 | ### 架构图 ```text ┌─────────────┐ 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_TOKEN` 或 `FMODE_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 实例共享同一个租户 申请成功后,你会拿到以下信息: ```text 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_SECRET` 和 `RELAY_PRIVATE_KEY` 是敏感信息,不要截图传播、不要提交到 Git。 ## 四、目标项目配置 ### 4.1 配置方式 目标项目使用文件存储配置,所有 Relay 配置写入: ``` outputs/webhook/relay-config.json ``` 你也可以通过 MCP 工具 `qiwei_relay_save_config` 写入。 ### 4.2 通过工具配置(推荐) 在 Claude Code 中调用: ```json { "name": "qiwei_relay_save_config", "arguments": { "relayBaseUrl": "http://8.138.37.248:4000", "tenantId": "你的租户ID(可选)", "publicKey": "对应的 RSA 公钥(可选,用于本地调试)" } } ``` > 注意:当前 `qiwei_relay_save_config` 工具只保存 `relayBaseUrl`、`tenantId`、`publicKey`,**不保存 `tenantApiKey`、`tenantApiSecret`、`privateKey`**。为了安全,后三者建议写入 `.env.local` 或环境变量。 ### 4.3 通过 .env.local 配置(推荐) 在目标项目根目录创建或编辑 `.env.local`: ```bash # 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.ts` 的 `getRelayPrivateKey()` 逻辑一致)。 ### 4.4 配置文件示例 `outputs/webhook/relay-config.json`: ```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.ts` 到 `mcp/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`): ```js 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` 中的私钥时,必须做换行还原: ```js 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`: ```bash 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 推荐启动流程 ```bash # 1. 确保 .env.local 已配置 Relay 凭证 # 2. 启动 MCP server(正常对话即可) # 3. 在另一个终端启动 Relay 客户端 node scripts/start-relay-client.js ``` 或封装为 npm script: ```json { "scripts": { "relay": "node scripts/start-relay-client.js" } } ``` ## 七、验证 Relay 是否正常工作 ### 7.1 检查配置 调用 MCP 工具: ```json { "name": "qiwei_relay_config" } ``` 应返回: ```json { "status": "ok", "data": { "configured": true, "relayBaseUrl": "http://8.138.37.248:4000" } } ``` ### 7.2 检查 Relay 客户端日志 启动 `scripts/start-relay-client.js` 后,观察日志: ```text [RelayClient] 启动 Relay 轮询 [RelayClient] 取回 3 条事件 [RelayClient] ACK 3 条事件 ``` ### 7.3 触发真实事件 让好友通过你的企微账号,或在客户群里发送一条消息。观察: - `outputs/webhook/events/` 目录下是否生成新事件文件 - `outputs/messages//` 是否出现新消息文件 - 画像文件 `outputs/portraits/.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 模式启用后,企微平台侧的回调地址应配置为: ```text {RELAY_BASE_URL}/api/webhook/ingest ``` 企微回调对服务 Token 全局生效。Skill 先把真实设备 `guid` 注册到 Relay,再调用 Fmode 专用接口 `POST /relay/connect`;由 Fmode 服务端持有回调密钥并调用 `/client/setCallback`。客户端不能提交回调 URL 或签名密钥。 旧的 `/{tenantId}/{guid}` 入口仅保留兼容,不用于新部署。 ## 九、安全注意事项 1. **不要把 `TENANT_API_SECRET` 和 `RELAY_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 节验证清单测试。 需要我继续执行实际的代码迁移吗?