73 lines
2.4 KiB
JavaScript
73 lines
2.4 KiB
JavaScript
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;
|