qiwei-relay-add-tenant-register-api-plan.md 15 KB


title: 在 Fmode 现有 Relay 上新增租户自助注册接口的实施计划 updated: 2026-07-27 project: claude-code-qiwe-assistant scope: server-side-relay

status: implemented

在 Fmode 现有 Relay 上新增租户自助注册接口的实施计划

本计划用于指导其他会话在 Fmode 现有 Relay 服务端(8.138.37.248)上新增一个租户自助注册接口。接口上线后,用户可通过调用该接口获取自己的 Relay 凭证,实现「一键开通 Relay」。

一、目标

在现有 Relay 服务端新增:

  1. POST /api/tenant/register:用户凭 Fmode token 自助注册 Relay 租户,返回完整凭证。
  2. 配套的数据库/文件存储结构,保存租户与 RSA 公钥。
  3. 鉴权与风控机制,防止滥用。

用户拿到返回的凭证后,可直接配置到本地 Skill:

RELAY_BASE_URL=http://8.138.37.248:4000
TENANT_API_KEY=<tenant-api-key>
TENANT_API_SECRET=<tenant-api-secret>
RELAY_PRIVATE_KEY=<pem-private-key-with-escaped-newlines>

二、前提条件

⚠️ 执行本计划前必须确认

  1. 拥有 Relay 服务端所在服务器(8.138.37.248)的 SSH/远程访问权限。
  2. 拥有 Relay 服务源代码的读取和修改权限。
  3. 拥有重新部署/重启 Relay 服务的权限。
  4. 了解当前 Relay 服务的技术栈(Node.js/Python/Go 等)。
  5. 拥有数据库修改权限(如果 Relay 使用数据库存储租户信息)。

如果以上任一条件不满足,应放弃本计划,改用「自建独立 Relay 服务端」方案。

三、当前 Relay 服务端现状(基于 Qiwei 项目反推)

从 Qiwei 项目配置可推断当前 Relay 服务端已具备以下能力:

  • 接收企微回调:POST /api/webhook/ingest/:tenantId/:guid
  • 长轮询取事件:POST /api/relay/poll
  • ACK 事件:POST /api/relay/ack
  • 租户鉴权:通过 Authorization: Bearer <TENANT_API_SECRET>
  • 事件加密:使用 RSA 公钥加密,本地私钥解密

但缺少:

  • 租户自助注册接口
  • 用户通过 Fmode token 自动开户的能力

四、接口设计

4.1 新增接口:POST /api/tenant/register

功能:用户使用 Fmode token 申请开通 Relay 租户,服务端生成凭证并返回。

请求头

POST /api/tenant/register
Content-Type: application/json
Authorization: Bearer <Fmode token>

请求体(可选):

{
  "description": "张三的本地 Skill",
  "deviceGuid": "可选,预注册设备"
}

响应体

{
  "success": true,
  "relayBaseUrl": "http://8.138.37.248:4000",
  "tenantId": "tenant_550e8400e29b41d4a716446655440000",
  "apiKey": "<generated-tenant-api-key>",
  "apiSecret": "<generated-tenant-api-secret>",
  "privateKey": "<generated-pem-private-key>",
  "createdAt": "2026-07-16T08:00:00.000Z"
}

错误响应

{
  "success": false,
  "error": "Fmode token 无效",
  "code": "INVALID_TOKEN"
}

4.2 新增接口:GET /api/tenant/status

功能:用户查询自己租户的状态和已用配额。

GET /api/tenant/status
Authorization: Bearer <TENANT_API_SECRET>

响应

{
  "success": true,
  "tenantId": "tenant_xxx",
  "createdAt": "2026-07-16T08:00:00.000Z",
  "eventCount24h": 1280,
  "pendingEventCount": 3
}

4.3 现有接口增强

  • /api/webhook/ingest/:tenantId/:guid:保持不变,继续接收企微回调。
  • /api/relay/poll:保持不变,继续使用 TENANT_API_SECRET 鉴权。
  • /api/relay/ack:保持不变。

五、数据库/存储变更

5.1 租户表(新增/扩展)

如果当前 Relay 已有租户表,增加字段;如果没有,新建表。

CREATE TABLE relay_tenants (
  tenant_id TEXT PRIMARY KEY,
  api_key TEXT UNIQUE NOT NULL,
  api_secret TEXT NOT NULL,           -- 生产环境建议存哈希
  api_secret_hash TEXT NOT NULL,      -- bcrypt/scrypt 哈希
  public_key TEXT NOT NULL,           -- RSA 公钥,用于加密事件
  fmode_user_id TEXT,                 -- 关联 Fmode 用户/公司(用于风控)
  description TEXT,
  max_devices INTEGER DEFAULT 10,     -- 最多绑定设备数
  daily_event_limit INTEGER DEFAULT 100000,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  is_active BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_relay_tenants_api_key ON relay_tenants(api_key);
CREATE INDEX idx_relay_tenants_fmode_user_id ON relay_tenants(fmode_user_id);

5.2 事件队列表

如果当前 Relay 使用文件队列,可保持不变;如果升级为数据库:

CREATE TABLE relay_events (
  event_id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL REFERENCES relay_tenants(tenant_id),
  device_guid TEXT,
  payload TEXT NOT NULL,              -- 事件原始 JSON
  encrypted_payload TEXT,             -- 可选:预加密存储
  received_at TIMESTAMP DEFAULT NOW(),
  acked_at TIMESTAMP,
  is_acked BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_relay_events_tenant_acked ON relay_events(tenant_id, is_acked);
CREATE INDEX idx_relay_events_device ON relay_events(tenant_id, device_guid);

六、实施步骤

步骤 1:备份现有 Relay 服务

  1. 登录 8.138.37.248 服务器。
  2. 备份当前 Relay 代码目录。
  3. 备份当前数据库/租户数据。
  4. 记录当前部署脚本和进程管理配置(PM2/systemd)。

步骤 2:修改 Relay 服务端代码

新增或修改以下模块:

6.2.1 租户生成模块

// relay-server/src/tenant-service.js
const crypto = require('crypto');

function generateTenant(fmodeUserId, description) {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
  });

  const tenantId = `tenant_${crypto.randomUUID().replace(/-/g, '')}`;
  const apiKey = `qk_${crypto.randomBytes(16).toString('hex')}`;
  const apiSecret = crypto.randomBytes(32).toString('hex');

  return {
    tenantId,
    apiKey,
    apiSecret,          // 明文返回给用户,只出现一次
    apiSecretHash: hashSecret(apiSecret), // 服务端存哈希
    publicKey,
    privateKey,         // 只返回给用户,服务端不存
    fmodeUserId,
    description,
    createdAt: new Date().toISOString()
  };
}

function hashSecret(secret) {
  return crypto.createHash('sha256').update(secret).digest('hex');
  // 或更安全的 bcrypt:require('bcrypt').hashSync(secret, 10)
}

module.exports = { generateTenant, hashSecret };

6.2.2 Fmode token 校验模块

// relay-server/src/fmode-auth.js
async function verifyFmodeToken(token) {
  // 方式 A:调 Fmode 网关的 /user/info 或 /validate 接口
  const response = await fetch('https://server.fmode.cn/api/auth/validate', {
    method: 'GET',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  if (!response.ok) throw new Error('Invalid Fmode token');
  return await response.json(); // 返回 userId / companyId 等
}

如果 Fmode 没有公开 token 校验接口,需要与 Fmode 后端协商,或改用「管理员手动审批」模式。

6.2.3 注册接口路由

// relay-server/src/routes/tenant.js
const express = require('express');
const router = express.Router();
const { generateTenant } = require('../tenant-service');
const { verifyFmodeToken } = require('../fmode-auth');
const db = require('../db');

router.post('/register', async (req, res) => {
  try {
    const auth = req.headers.authorization || '';
    const token = auth.replace(/^Bearer\s+/i, '');
    if (!token) return res.status(401).json({ success: false, error: '缺少 Fmode token' });

    // 1. 校验 Fmode token
    const fmodeUser = await verifyFmodeToken(token);

    // 2. 风控:检查该用户是否已注册
    const existing = await db.findTenantByFmodeUserId(fmodeUser.userId);
    if (existing) {
      return res.status(409).json({
        success: false,
        error: '该 Fmode 账号已开通 Relay',
        tenantId: existing.tenant_id
      });
    }

    // 3. 生成租户
    const tenant = generateTenant(fmodeUser.userId, req.body.description);

    // 4. 保存到数据库(不存私钥和明文 apiSecret)
    await db.createTenant({
      tenant_id: tenant.tenantId,
      api_key: tenant.apiKey,
      api_secret_hash: tenant.apiSecretHash,
      public_key: tenant.publicKey,
      fmode_user_id: tenant.fmodeUserId,
      description: tenant.description,
      created_at: tenant.createdAt
    });

    // 5. 返回凭证(私钥只返回这一次)
    res.json({
      success: true,
      relayBaseUrl: process.env.RELAY_PUBLIC_URL || `http://${req.headers.host}`,
      tenantId: tenant.tenantId,
      apiKey: tenant.apiKey,
      apiSecret: tenant.apiSecret,
      privateKey: tenant.privateKey,
      createdAt: tenant.createdAt
    });
  } catch (err) {
    console.error('[Relay] 租户注册失败:', err.message);
    res.status(500).json({ success: false, error: err.message });
  }
});

module.exports = router;

6.2.4 修改鉴权中间件

现有 /api/relay/poll/api/relay/ack 通过 TENANT_API_SECRET 鉴权。如果之前是明文比对,改为哈希比对:

function authenticateTenant(req, res, next) {
  const auth = req.headers.authorization || '';
  const secret = auth.replace(/^Bearer\s+/i, '');
  const tenant = db.findTenantByApiSecret(secret); // 内部用 hash 比对
  if (!tenant) return res.status(401).json({ error: 'unauthorized' });
  req.tenant = tenant;
  next();
}

app.use('/api/relay/poll', authenticateTenant);
app.use('/api/relay/ack', authenticateTenant);

步骤 3:配置环境变量

在 Relay 服务端新增:

# Relay 公网地址
RELAY_PUBLIC_URL=http://8.138.37.248:4000

# 管理员接口密钥(可选,用于 /api/admin/*)
ADMIN_TOKEN=your_very_long_random_admin_token

# Fmode 网关地址
FMODE_GATEWAY_URL=https://server.fmode.cn

步骤 4:数据库迁移

执行第 5 节的 SQL,创建/扩展租户表和事件表。

步骤 5:部署与重启

  1. 上传修改后的代码到服务器。
  2. 安装新增依赖(如 bcryptnode-fetch 等)。
  3. 执行数据库迁移。
  4. 重启 Relay 服务。
  5. 检查日志确认启动成功。

步骤 6:接口测试

6.6.1 测试注册接口

curl -X POST http://8.138.37.248:4000/api/tenant/register \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <Fmode token>" \
  -d '{"description":"测试租户"}'

期望返回包含 tenantIdapiKeyapiSecretprivateKeyrelayBaseUrl

6.6.2 测试 poll 接口

curl -X POST http://8.138.37.248:4000/api/relay/poll \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <TENANT_API_SECRET>" \
  -d '{"guid":"your-device-guid","batchSize":10}'

期望返回 { "success": true, "events": [] }

6.6.3 测试企微回调链路

  1. 使用 Token 级全局回调地址:

    http://8.138.37.248:4000/api/webhook/ingest
    
  2. 调用 Fmode 专用接口 POST /api/qiwei/relay/connect,由服务端配置该地址和签名密钥。

  3. 触发一个企微事件(如发送群消息)。

  4. 调用 /api/relay/poll,应能取回加密事件。

  5. 用返回的 privateKey 解密验证。

七、本地 Skill 侧对接

接口上线后,本地 Skill(目标项目)需要增强以下能力:

  1. 新增工具 qiwei_relay_register:调用 /api/tenant/register,自动保存返回的凭证。
  2. 增强 qiwei_relay_save_config:支持保存 tenantApiKeytenantApiSecretrelayPrivateKey
  3. 增强 qiwei_relay_connect:保存凭证后自动启动 Relay 长轮询客户端。
  4. 新增 scripts/start-relay-client.js:独立进程运行长轮询。

用户侧的使用流程:

1. 用户调用 qiwei_relay_register
   输入:Fmode token
   输出:Relay 凭证

2. Skill 自动把凭证写入 .env.local 和 outputs/webhook/relay-config.json

3. 用户调用 qiwei_relay_connect
   Skill 启动 relay-client.js 长轮询

4. 用户调用 qiwei_webhook_auto_setup
   Skill 注册设备后调用 Fmode 专用接口;服务端配置全局回调:
   http://8.138.37.248:4000/api/webhook/ingest

5. 用户正常使用,实时事件自动推送到本地

八、安全与风控要求

  1. Fmode token 校验:注册接口必须校验 Fmode token,否则任何人都能批量开户。
  2. 单个用户限制:一个 Fmode 用户只能注册一个租户,防止滥用。
  3. 租户配额:限制每个租户的设备数、每日事件数、队列长度。
  4. 私钥只返回一次:服务端不保存私钥,丢失后只能重置租户。
  5. apiSecret 存哈希:服务端永远不要存明文 apiSecret
  6. HTTPS 强制:生产环境注册接口必须走 HTTPS,私钥传输不能明文。
  7. 限流:注册接口按 IP 和 Fmode 用户限流,防止刷接口。
  8. 审计日志:记录每次注册、重置、删除租户的操作。

九、回滚方案

如果接口上线后出现问题:

  1. 立即回滚到上一版本代码。
  2. 恢复旧版数据库(如果迁移失败)。
  3. 关闭 /api/tenant/register 接口访问(防火墙或路由层)。
  4. 通知已注册用户凭证失效,等待修复后重新注册。

十、风险与限制

风险 说明 应对
无服务器权限 如果无法访问 8.138.37.248,本计划无法执行 改用自建 Relay 服务端
Fmode token 校验困难 如果 Fmode 没有公开校验接口 与 Fmode 协商,或改为管理员审批模式
私钥丢失 用户丢失私钥后无法解密历史事件 提供重置接口(重新生成密钥对,旧事件废弃)
单点故障 Relay 服务端宕机,所有用户收不到事件 部署多实例 + 负载均衡 + 监控告警
数据泄露 事件 payload 包含聊天记录 RSA 加密 + HTTPS + 服务端不存私钥

十一、如果无法修改 Fmode 现有 Relay

如果最终发现没有 8.138.37.248 的修改权限,应立即切换到备用方案:

自建独立 Relay 服务端

  • 在自己可控的服务器上部署全新的 Relay 服务。
  • 提供 /api/tenant/register/api/webhook/ingest/api/relay/poll/api/relay/ack
  • 企微回调地址指向你的服务器。

具体实现可参考 docs/specs/qiwei-relay-mode-guide.md 中的最小 Relay 服务端示例。

十二、执行清单

  • 确认拥有 8.138.37.248 服务器访问权限
  • 备份现有 Relay 代码和数据
  • 新增 /api/tenant/register 接口
  • 新增 /api/tenant/status 接口
  • 扩展租户表结构
  • 实现 Fmode token 校验
  • 实现 apiSecret 哈希存储
  • 修改 poll/ack 鉴权为哈希比对
  • 配置环境变量
  • 执行数据库迁移
  • 部署并重启 Relay 服务
  • 测试注册接口
  • 测试 poll 接口
  • 测试完整回调链路
  • 增强本地 Skill 的 qiwei_relay_register 工具
  • 更新相关文档

执行本计划后,用户确实可以通过调用接口获取到自己的 Relay 凭证。但请务必先确认服务器权限,否则应改用自建 Relay 方案。