import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { type ClaimableDedupe, createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe"; import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import type { NormalizedWebhookMessage } from "./monitor-normalize.js";
// BlueBubbles has no sequence/ack in its webhook protocol, and its // MessagePoller replays its ~1-week lookback window as `new-message` events // after BB Server restarts or reconnects. Without persistent dedup, the // gateway can reply to messages that were already handled before a restart // (see issues #19176, #12053). // // TTL matches BB's lookback window so any replay is guaranteed to land on // a remembered GUID, and the file-backed store survives gateway restarts. const DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1_000; const MEMORY_MAX_SIZE = 5_000; const FILE_MAX_ENTRIES = 50_000; // Cap GUID length so a malformed or hostile payload can't bloat the on-disk // dedupe file. Real BB GUIDs are short (<64 chars); 512 is generous. const MAX_GUID_CHARS = 512;
function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string { if (env.VITEST || env.NODE_ENV === "test") { // Isolate tests from real ~/.openclaw state without sharing across tests. // Stable-per-pid so the scoped dedupe test can observe persistence. const name = "openclaw-vitest-" + process.pid; return path.join(resolvePreferredOpenClawTmpDir(), name);
} // Canonical OpenClaw state dir: honors OPENCLAW_STATE_DIR (with `~` expansion // via resolveUserPath), plus legacy/new fallback. Using the shared helper // keeps this plugin's persistence aligned with the rest of OpenClaw state. return resolveStateDir(env);
}
function resolveNamespaceFilePath(namespace: string): string { // Keep a readable prefix for operator debugging, but suffix with a short // hash of the raw namespace so account IDs that only differ by // filesystem-unsafe characters (e.g. "acct/a" vs "acct:a") don't collapse // onto the same file. const safePrefix = namespace.replace(/[^a-zA-Z0-9_-]/g, "_") || "ns"; const hash = createHash("sha256").update(namespace, "utf8").digest("hex").slice(0, 12); const dir = path.join(resolveStateDirFromEnv(), "bluebubbles", "inbound-dedupe"); const newPath = path.join(dir, `${safePrefix}__${hash}.json`);
// One-time migration: earlier beta shipped `${safe}.json` (no hash). // Rename so the upgrade preserves existing dedupe entries instead of // starting from an empty file and replaying already-handled messages.
migrateLegacyDedupeFile(namespace, newPath);
return newPath;
}
const migratedNamespaces = new Set<string>();
function migrateLegacyDedupeFile(namespace: string, newPath: string): void { if (migratedNamespaces.has(namespace)) { return;
}
migratedNamespaces.add(namespace); try { const legacyPath = resolveLegacyNamespaceFilePath(namespace); if (legacyPath === newPath) { return;
} if (!fs.existsSync(legacyPath)) { return;
} if (!fs.existsSync(newPath)) {
fs.renameSync(legacyPath, newPath);
} else { // Both exist: new file is authoritative; remove the stale legacy.
fs.unlinkSync(legacyPath);
}
} catch { // Best-effort migration; a missed rename is strictly less harmful // than crashing the module load path.
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.