149 lines
4.2 KiB
JavaScript
Raw Permalink Normal View History

/**
* services/zhuyuan-bridge/server.js
*
* 铸渊桥接服务 Webhook Server可选增强
*
* 功能
* - 接收 GitHub App Webhook 事件
* - 健康检查端点
* - 实时响应不经过 Issue 中转
*
* 部署
* pm2 start server.js --name zhuyuan-bridge
*
* 环境变量
* GHAPP_WEBHOOK_SECRET GitHub App Webhook Secret
* PORT 服务端口默认 3800
*/
'use strict';
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3800;
// ===== Webhook 签名验证 =====
function verifySignature(payload, signature) {
if (!signature || !process.env.GHAPP_WEBHOOK_SECRET) return false;
const hmac = crypto.createHmac('sha256', process.env.GHAPP_WEBHOOK_SECRET);
hmac.update(payload);
const expected = 'sha256=' + hmac.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch (_) {
return false;
}
}
// 解析 JSON body但保留原始 buffer 供签名验证
app.use(function(req, res, next) {
let rawBody = '';
req.on('data', function(chunk) { rawBody += chunk; });
req.on('end', function() {
req.rawBody = rawBody;
try {
req.body = JSON.parse(rawBody);
} catch (_) {
req.body = {};
}
next();
});
});
// ===== 简易速率限制 =====
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 30; // max requests per window
function rateLimit(req, res, next) {
const ip = req.ip || req.connection.remoteAddress || 'unknown';
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now - entry.start > RATE_LIMIT_WINDOW) {
rateLimitMap.set(ip, { start: now, count: 1 });
return next();
}
entry.count++;
if (entry.count > RATE_LIMIT_MAX) {
res.status(429).json({ error: 'Too many requests' });
return;
}
next();
}
// ===== 健康检查 =====
app.get('/health', function(req, res) {
res.json({
status: 'alive',
agent: 'zhuyuan-bridge',
version: '1.0.0',
timestamp: new Date().toISOString()
});
});
// ===== GitHub App Webhook 入口 =====
app.post('/webhook/github-app', rateLimit, function(req, res) {
const event = req.headers['x-github-event'];
const sig = req.headers['x-hub-signature-256'];
// 签名验证(如果配置了 secret
if (process.env.GHAPP_WEBHOOK_SECRET) {
if (!verifySignature(req.rawBody, sig)) {
console.warn('[铸渊桥接] ⚠️ Webhook 签名验证失败');
res.status(401).json({ error: 'Signature mismatch' });
return;
}
}
console.log('[铸渊桥接] 收到事件:', event);
// 立即响应 200避免 GitHub 超时)
res.status(200).json({ received: true, event: event });
// 异步处理事件
handleWebhookEvent(event, req.body).catch(function(err) {
console.error('[铸渊桥接] 事件处理失败:', err.message);
});
});
/**
* 处理 Webhook 事件
*/
async function handleWebhookEvent(event, payload) {
switch (event) {
case 'issues':
if (payload.action === 'opened' || payload.action === 'labeled') {
const labels = (payload.issue.labels || []).map(function(l) { return l.name; });
if (labels.includes('zhuyuan-exec')) {
console.log('[铸渊桥接] 检测到执行指令 Issue #' + payload.issue.number);
// 在 Webhook 模式下,可以直接启动执行
// 但默认推荐通过 Workflow 执行(更安全、有日志)
console.log('[铸渊桥接] 由 GitHub Actions Workflow 处理执行');
}
}
break;
case 'push':
console.log('[铸渊桥接] Push 事件 → ' + (payload.ref || ''));
break;
case 'pull_request':
console.log('[铸渊桥接] PR 事件 → #' + (payload.pull_request ? payload.pull_request.number : '?'));
break;
default:
console.log('[铸渊桥接] 未处理的事件类型:', event);
}
}
// ===== 启动服务 =====
app.listen(PORT, function() {
console.log('[铸渊桥接] 🚀 启动在端口 ' + PORT);
console.log('[铸渊桥接] 健康检查: http://localhost:' + PORT + '/health');
console.log('[铸渊桥接] Webhook: http://localhost:' + PORT + '/webhook/github-app');
});