149 lines
4.2 KiB
JavaScript
149 lines
4.2 KiB
JavaScript
/**
|
||
* 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');
|
||
});
|