mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: tighten feishu dedupe boundary (#23377) (thanks @SidQin-cyber)
This commit is contained in:
@@ -20,7 +20,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.
|
||||
- Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started `signal-cli` is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn.
|
||||
- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths.
|
||||
- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. (#23377) Thanks @SidQin-cyber.
|
||||
- ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen.
|
||||
- Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`).
|
||||
- Security/Exec env: block request-scoped `HOME` and `ZDOTDIR` overrides in host exec env sanitizers (Node + macOS), preventing shell startup-file execution before allowlist-evaluated command bodies. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_DEDUP_DIR = path.join(os.homedir(), ".openclaw", "feishu", "dedup");
|
||||
const MAX_ENTRIES_PER_FILE = 10_000;
|
||||
const CLEANUP_PROBABILITY = 0.02;
|
||||
const CACHE_STALE_MS = 30_000;
|
||||
|
||||
type DedupData = Record<string, number>;
|
||||
|
||||
/**
|
||||
* Filesystem-backed dedup store. Each "namespace" (typically a Feishu account
|
||||
* ID) maps to a single JSON file containing `{ messageId: timestampMs }` pairs.
|
||||
*
|
||||
* Writes use atomic rename to avoid partial-read corruption. Probabilistic
|
||||
* cleanup keeps the file size bounded without adding latency to every call.
|
||||
*/
|
||||
export class DedupStore {
|
||||
private readonly dir: string;
|
||||
private cache = new Map<string, DedupData>();
|
||||
private cacheLoadedAt = new Map<string, number>();
|
||||
|
||||
constructor(dir?: string) {
|
||||
this.dir = dir ?? DEFAULT_DEDUP_DIR;
|
||||
}
|
||||
|
||||
private filePath(namespace: string): string {
|
||||
const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
return path.join(this.dir, `${safe}.json`);
|
||||
}
|
||||
|
||||
async load(namespace: string): Promise<DedupData> {
|
||||
const loadedAt = this.cacheLoadedAt.get(namespace);
|
||||
if (loadedAt != null && Date.now() - loadedAt > CACHE_STALE_MS) {
|
||||
this.cache.delete(namespace);
|
||||
this.cacheLoadedAt.delete(namespace);
|
||||
}
|
||||
|
||||
const cached = this.cache.get(namespace);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const raw = await fs.promises.readFile(this.filePath(namespace), "utf-8");
|
||||
const data: DedupData = JSON.parse(raw);
|
||||
this.cache.set(namespace, data);
|
||||
this.cacheLoadedAt.set(namespace, Date.now());
|
||||
return data;
|
||||
} catch {
|
||||
const data: DedupData = {};
|
||||
this.cache.set(namespace, data);
|
||||
this.cacheLoadedAt.set(namespace, Date.now());
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async has(namespace: string, messageId: string, ttlMs: number): Promise<boolean> {
|
||||
const data = await this.load(namespace);
|
||||
const ts = data[messageId];
|
||||
if (ts == null) return false;
|
||||
if (Date.now() - ts > ttlMs) {
|
||||
// Expired — treat as absent. Skip the delete here to avoid silent
|
||||
// cache/disk divergence; actual cleanup happens probabilistically
|
||||
// inside record().
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async record(namespace: string, messageId: string, ttlMs: number): Promise<void> {
|
||||
const data = await this.load(namespace);
|
||||
data[messageId] = Date.now();
|
||||
|
||||
if (Math.random() < CLEANUP_PROBABILITY) {
|
||||
this.evict(data, ttlMs);
|
||||
}
|
||||
|
||||
await this.flush(namespace, data);
|
||||
}
|
||||
|
||||
private evict(data: DedupData, ttlMs: number): void {
|
||||
const now = Date.now();
|
||||
for (const key of Object.keys(data)) {
|
||||
if (now - data[key] > ttlMs) delete data[key];
|
||||
}
|
||||
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length > MAX_ENTRIES_PER_FILE) {
|
||||
keys
|
||||
.sort((a, b) => data[a] - data[b])
|
||||
.slice(0, keys.length - MAX_ENTRIES_PER_FILE)
|
||||
.forEach((k) => delete data[k]);
|
||||
}
|
||||
}
|
||||
|
||||
private async flush(namespace: string, data: DedupData): Promise<void> {
|
||||
await fs.promises.mkdir(this.dir, { recursive: true });
|
||||
const fp = this.filePath(namespace);
|
||||
const tmp = `${fp}.tmp.${process.pid}`;
|
||||
await fs.promises.writeFile(tmp, JSON.stringify(data), "utf-8");
|
||||
await fs.promises.rename(tmp, fp);
|
||||
this.cacheLoadedAt.set(namespace, Date.now());
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,6 @@ function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
|
||||
if (stateOverride) {
|
||||
return stateOverride;
|
||||
}
|
||||
if (env.VITEST || env.NODE_ENV === "test") {
|
||||
return path.join(os.tmpdir(), "openclaw-vitest-" + process.pid);
|
||||
}
|
||||
return path.join(os.homedir(), ".openclaw");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user