const crypto = require('crypto'); const express = require('express'); const { ContentIngestService, ContentIngestError } = require('../services/ContentIngestService'); const router = express.Router(); const requests = new Map(); const RATE_LIMIT = 30; const WINDOW_MS = 60 * 1000; const sendError = (res, status, code, message, retryable = false) => res.status(status).json({ success: false, error: { code, message, retryable }, }); const requireContentToken = (req, res, next) => { const configuredToken = process.env.CONTENT_INGEST_TOKEN; if (!configuredToken) { return sendError(res, 503, 'SERVICE_UNAVAILABLE', '内容发布接口尚未配置', true); } const header = req.get('authorization') || ''; const match = header.match(/^Bearer\s+(.+)$/i); if (!match) return sendError(res, 401, 'UNAUTHORIZED', '缺少或无效的访问凭据'); const received = Buffer.from(match[1]); const expected = Buffer.from(configuredToken); if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) { return sendError(res, 401, 'UNAUTHORIZED', '缺少或无效的访问凭据'); } return next(); }; const rateLimit = (req, res, next) => { const key = req.ip || req.socket.remoteAddress || 'unknown'; const now = Date.now(); const state = requests.get(key) || { start: now, count: 0 }; if (now - state.start >= WINDOW_MS) { state.start = now; state.count = 0; } state.count += 1; requests.set(key, state); if (state.count > RATE_LIMIT) { return sendError(res, 429, 'RATE_LIMITED', '请求过于频繁,请稍后重试', true); } return next(); }; router.use(rateLimit, requireContentToken); router.get('/health', (req, res) => { res.json({ status: 'ok', service: 'amaba-ai-website', site_profile: 'ai', api_version: 'v1', }); }); router.post('/articles/upsert', async (req, res) => { try { const result = await ContentIngestService.upsert(req.body, req.get('Idempotency-Key')); res.status(result.statusCode).json(result.body); } catch (error) { if (error instanceof ContentIngestError) { return sendError(res, error.status, error.code, error.message, error.status >= 500); } // 不记录请求正文或 Authorization,避免 Token 和终稿进入日志。 console.error('内容发布接口失败:', error.message); return sendError(res, 500, 'INTERNAL_ERROR', '内容发布暂时失败,请稍后重试', true); } }); module.exports = router;