| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- const fs = require('fs');
- const path = require('path');
- const crypto = require('crypto');
- const { categoryDir, createRunDir, writeRunManifest } = require('./output-paths');
- const PROCESSED_EVENT_IDS_FILE = path.join(categoryDir('webhook'), 'processed-event-ids.json');
- const MAX_PROCESSED_IDS = 5000;
- function safeReadJson(filePath, fallback = null) {
- try {
- if (!filePath || !fs.existsSync(filePath)) return fallback;
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- } catch {
- return fallback;
- }
- }
- function atomicWriteJson(filePath, data) {
- const dir = path.dirname(filePath);
- fs.mkdirSync(dir, { recursive: true });
- const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
- try {
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');
- fs.renameSync(tmpPath, filePath);
- } catch (err) {
- try { fs.unlinkSync(tmpPath); } catch {}
- throw err;
- }
- return filePath;
- }
- function sanitizePayload(payload) {
- if (!payload || typeof payload !== 'object') return payload;
- const output = {};
- for (const [key, value] of Object.entries(payload)) {
- if (/^(token|tokenId|secret|signature|privateKey|authorization)$/i.test(key)) {
- output[key] = '••••••••';
- continue;
- }
- if (value && typeof value === 'object') {
- output[key] = sanitizePayload(value);
- } else {
- output[key] = value;
- }
- }
- return output;
- }
- function readProcessedEventIds() {
- const data = safeReadJson(PROCESSED_EVENT_IDS_FILE, { ids: [], count: 0 });
- const ids = Array.isArray(data.ids) ? data.ids : [];
- return new Set(ids);
- }
- function writeProcessedEventIds(set) {
- let ids = Array.from(set);
- if (ids.length > MAX_PROCESSED_IDS) {
- ids = ids.slice(ids.length - MAX_PROCESSED_IDS);
- }
- atomicWriteJson(PROCESSED_EVENT_IDS_FILE, {
- ids,
- count: ids.length,
- updatedAt: new Date().toISOString()
- });
- return ids;
- }
- function eventScopeKey(eventId, account = {}) {
- const userId = String(account.userId || '').trim();
- return userId ? `${userId}:${eventId}` : String(eventId || '');
- }
- function markEventProcessed(eventId, account = {}) {
- const set = readProcessedEventIds();
- set.add(eventScopeKey(eventId, account));
- writeProcessedEventIds(set);
- return true;
- }
- function isDuplicateEvent(eventId, account = {}) {
- const set = readProcessedEventIds();
- return set.has(eventScopeKey(eventId, account));
- }
- function saveWebhookEventStructured(event, source = 'callback', rawBody = null) {
- const runDir = createRunDir('webhook', source);
- const eventId = event.eventId || `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
- const fileName = `event-${eventId}.json`;
- const filePath = path.join(runDir, fileName);
- const record = {
- receivedAt: new Date().toISOString(),
- source,
- eventId,
- parsedType: event.parsedType,
- cmd: event.cmd,
- msgType: event.msgType,
- guid: event.guid,
- externalUserId: event.externalUserId || undefined,
- status: 'PENDING',
- event: sanitizePayload(event.raw),
- rawBody: rawBody ? sanitizePayload(typeof rawBody === 'string' ? JSON.parse(rawBody) : rawBody) : undefined
- };
- atomicWriteJson(filePath, record);
- writeRunManifest(runDir, {
- tool: 'webhook-receiver',
- summary: { eventId, parsedType: event.parsedType, source },
- files: [fileName]
- });
- return filePath;
- }
- function saveRawEventFailed(body, source, reason, rawBody = null) {
- const runDir = createRunDir('webhook', `${source}-failed`);
- const hash = crypto.createHash('sha256').update(JSON.stringify(body || {})).digest('hex').slice(0, 16);
- const fileName = `event-failed-${hash}.json`;
- const filePath = path.join(runDir, fileName);
- const record = {
- receivedAt: new Date().toISOString(),
- source,
- reason,
- status: 'FAILED',
- body: sanitizePayload(body),
- rawBody: rawBody ? sanitizePayload(typeof rawBody === 'string' ? JSON.parse(rawBody) : rawBody) : undefined
- };
- atomicWriteJson(filePath, record);
- writeRunManifest(runDir, {
- tool: 'webhook-receiver',
- summary: { reason, source },
- files: [fileName]
- });
- return filePath;
- }
- function updateWebhookEventStatus(eventFilePath, status, result = null) {
- if (!eventFilePath || !fs.existsSync(eventFilePath)) return null;
- try {
- const record = safeReadJson(eventFilePath) || {};
- record.status = status;
- record.processedAt = new Date().toISOString();
- if (result !== null) record.result = result;
- atomicWriteJson(eventFilePath, record);
- return record;
- } catch (err) {
- console.warn('[WebhookStore] 更新事件状态失败:', err.message);
- return null;
- }
- }
- module.exports = {
- PROCESSED_EVENT_IDS_FILE,
- readProcessedEventIds,
- writeProcessedEventIds,
- markEventProcessed,
- isDuplicateEvent,
- saveWebhookEventStructured,
- saveRawEventFailed,
- updateWebhookEventStatus,
- sanitizePayload
- };
|