新增内容引擎文章发布接口与详情页

This commit is contained in:
amb
2026-09-11 17:19:09 +08:00
parent ba6aab722e
commit 726485dccb
16 changed files with 958 additions and 102 deletions
+72
View File
@@ -0,0 +1,72 @@
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;