/** * Open API Access Logger — JSONL 日志中间件 * * 将每次 /api/open/* 请求记录为一条 JSON 行。 * * 目录结构: * apiconversation/ * 2026-03-11/ * access.jsonl ← 普通访问日志 (search, download, list) * job-xxx-aaa.jsonl ← 单个 Job 的完整生命周期日志 * 2026-03-12/ * ... */ const fs = require('fs'); const path = require('path'); const LOG_DIR = path.resolve(__dirname, '../../apiconversation'); /** * Express 中间件:记录 Open API 请求 */ function apiAccessLogger(req, res, next) { const start = Date.now(); // 拦截 res.end 以获取状态码 const originalEnd = res.end; res.end = function (...args) { const latency = Date.now() - start; const apiKey = req.apiKey || extractKeyHint(req); const fullPath = '/api/open' + req.path; const entry = { ts: new Date().toISOString(), key: maskKey(apiKey), method: req.method, path: fullPath, action: describeAction(req.method, req.path, req.query), query: Object.keys(req.query).length > 0 ? req.query : undefined, body: summarizeBody(req), status: res.statusCode, statusText: describeStatus(res.statusCode), latency, ip: req.ip || req.connection?.remoteAddress, userAgent: req.headers['user-agent']?.substring(0, 100), }; // 异步写入,不阻塞响应 writeLog(entry, req.path); originalEnd.apply(res, args); }; next(); } /** * 从请求头提取 Key 提示(用于 401 场景下 req.apiKey 不存在时) */ function extractKeyHint(req) { const auth = req.headers['authorization']; if (auth && auth.startsWith('Bearer ')) return auth.slice(7).trim(); return req.headers['x-api-key'] || null; } /** * 遮蔽 API Key(只显示前 6 位和后 3 位) */ function maskKey(key) { if (!key) return ''; if (key.length <= 10) return key.substring(0, 3) + '***'; return key.substring(0, 6) + '***' + key.substring(key.length - 3); } /** * 提取请求 body 的摘要(仅对 POST 请求,截断到 200 字) */ function summarizeBody(req) { if (req.method !== 'POST' || !req.body) return undefined; const s = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); return s.length > 200 ? s.substring(0, 200) + '...' : s; } /** * 判断请求路径对应的 Job ID(如果有的话) */ function extractJobId(reqPath) { // /jobs/:jobId 或 /jobs/:jobId/result const jobMatch = reqPath.match(/^\/jobs\/(job-[^/]+)/); if (jobMatch) return jobMatch[1]; return null; } /** * 写一条日志:普通请求 → access.jsonl,Job 相关 → job-{id}.jsonl */ function writeLog(entry, reqPath) { try { const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const dateDir = path.join(LOG_DIR, date); fs.mkdirSync(dateDir, { recursive: true }); const jobId = extractJobId(reqPath); const logFile = jobId ? path.join(dateDir, `${jobId}.jsonl`) : path.join(dateDir, 'access.jsonl'); const line = JSON.stringify(entry) + '\n'; fs.appendFileSync(logFile, line, 'utf-8'); } catch (err) { console.warn('[OpenAPI Logger] Failed to write log:', err.message); } } /** * 根据请求路径生成可读的操作描述 */ function describeAction(method, reqPath, query) { if (reqPath.startsWith('/skills/search')) { return `搜索 Skill: "${query?.q || ''}"`; } if (reqPath.match(/^\/skills\/[^/]+\/download$/)) { const name = reqPath.split('/')[2]; return `下载 Skill: ${name}`; } if (reqPath.match(/^\/skills\/[^/]+\/feedback$/) && method === 'POST') { const name = reqPath.split('/')[2]; return `提交反馈: ${name}`; } if (reqPath.match(/^\/skills\/[^/]+\/updates$/) && method === 'GET') { const name = reqPath.split('/')[2]; return `检查更新: ${name}`; } if (reqPath.match(/^\/skills\/[^/]+$/) && method === 'GET') { const name = reqPath.split('/')[2]; return `查看 Skill 详情: ${name}`; } if (reqPath === '/skills/create' && method === 'POST') { return '请求创建 Skill'; } if (reqPath.match(/^\/jobs\/[^/]+\/result$/)) { return '下载 Job 结果'; } if (reqPath.match(/^\/jobs\/[^/]+$/) && method === 'GET') { const jobId = reqPath.split('/')[2]; return `查询 Job 状态: ${jobId.substring(0, 16)}`; } if (reqPath === '/jobs' && method === 'GET') { return '列出 Jobs'; } return `${method} ${reqPath}`; } /** * 状态码可读描述 */ function describeStatus(code) { const map = { 200: '成功', 201: '已创建', 202: '已接受', 204: '无内容', 400: '请求错误', 401: '未授权', 403: '禁止访问', 404: '未找到', 500: '服务器错误', 502: '网关错误', 503: '服务不可用', }; return map[code] || (code < 400 ? '成功' : '错误'); } module.exports = { apiAccessLogger };