title: 在 Fmode 现有 Relay 上新增租户自助注册接口的实施计划 updated: 2026-07-27 project: claude-code-qiwe-assistant scope: server-side-relay
本计划用于指导其他会话在 Fmode 现有 Relay 服务端(
8.138.37.248)上新增一个租户自助注册接口。接口上线后,用户可通过调用该接口获取自己的 Relay 凭证,实现「一键开通 Relay」。
在现有 Relay 服务端新增:
POST /api/tenant/register:用户凭 Fmode token 自助注册 Relay 租户,返回完整凭证。用户拿到返回的凭证后,可直接配置到本地 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>
⚠️ 执行本计划前必须确认:
8.138.37.248)的 SSH/远程访问权限。如果以上任一条件不满足,应放弃本计划,改用「自建独立 Relay 服务端」方案。
从 Qiwei 项目配置可推断当前 Relay 服务端已具备以下能力:
POST /api/webhook/ingest/:tenantId/:guidPOST /api/relay/pollPOST /api/relay/ackAuthorization: Bearer <TENANT_API_SECRET>但缺少:
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"
}
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
}
/api/webhook/ingest/:tenantId/:guid:保持不变,继续接收企微回调。/api/relay/poll:保持不变,继续使用 TENANT_API_SECRET 鉴权。/api/relay/ack:保持不变。如果当前 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);
如果当前 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);
8.138.37.248 服务器。新增或修改以下模块:
// 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 };
// 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 后端协商,或改用「管理员手动审批」模式。
// 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;
现有 /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);
在 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
执行第 5 节的 SQL,创建/扩展租户表和事件表。
bcrypt、node-fetch 等)。curl -X POST http://8.138.37.248:4000/api/tenant/register \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <Fmode token>" \
-d '{"description":"测试租户"}'
期望返回包含 tenantId、apiKey、apiSecret、privateKey、relayBaseUrl。
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": [] }。
使用 Token 级全局回调地址:
http://8.138.37.248:4000/api/webhook/ingest
调用 Fmode 专用接口 POST /api/qiwei/relay/connect,由服务端配置该地址和签名密钥。
触发一个企微事件(如发送群消息)。
调用 /api/relay/poll,应能取回加密事件。
用返回的 privateKey 解密验证。
接口上线后,本地 Skill(目标项目)需要增强以下能力:
qiwei_relay_register:调用 /api/tenant/register,自动保存返回的凭证。qiwei_relay_save_config:支持保存 tenantApiKey、tenantApiSecret、relayPrivateKey。qiwei_relay_connect:保存凭证后自动启动 Relay 长轮询客户端。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. 用户正常使用,实时事件自动推送到本地
apiSecret。如果接口上线后出现问题:
/api/tenant/register 接口访问(防火墙或路由层)。| 风险 | 说明 | 应对 |
|---|---|---|
| 无服务器权限 | 如果无法访问 8.138.37.248,本计划无法执行 |
改用自建 Relay 服务端 |
| Fmode token 校验困难 | 如果 Fmode 没有公开校验接口 | 与 Fmode 协商,或改为管理员审批模式 |
| 私钥丢失 | 用户丢失私钥后无法解密历史事件 | 提供重置接口(重新生成密钥对,旧事件废弃) |
| 单点故障 | Relay 服务端宕机,所有用户收不到事件 | 部署多实例 + 负载均衡 + 监控告警 |
| 数据泄露 | 事件 payload 包含聊天记录 | RSA 加密 + HTTPS + 服务端不存私钥 |
如果最终发现没有 8.138.37.248 的修改权限,应立即切换到备用方案:
自建独立 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 服务器访问权限/api/tenant/register 接口/api/tenant/status 接口qiwei_relay_register 工具执行本计划后,用户确实可以通过调用接口获取到自己的 Relay 凭证。但请务必先确认服务器权限,否则应改用自建 Relay 方案。