diff --git a/extensions/feishu/src/dedup.test.ts b/extensions/feishu/src/dedup.test.ts index 1b3f1c749c82..6fc57f4ef4d3 100644 --- a/extensions/feishu/src/dedup.test.ts +++ b/extensions/feishu/src/dedup.test.ts @@ -12,7 +12,7 @@ import { hasProcessedFeishuMessage, testingHooks, tryRecordMessagePersistent, - warmupDedupFromDisk, + warmupDedupFromPluginState, } from "./dedup.js"; import { setFeishuRuntime } from "./runtime.js"; @@ -59,7 +59,7 @@ describe("Feishu persistent dedupe", () => { await expect(tryRecordMessagePersistent("msg-2", "account-a")).resolves.toBe(true); testingHooks.resetFeishuDedupMemoryForTests(); - await expect(warmupDedupFromDisk("account-a")).resolves.toBe(1); + await expect(warmupDedupFromPluginState("account-a")).resolves.toBe(1); await expect(tryRecordMessagePersistent("msg-2", "account-a")).resolves.toBe(false); }); @@ -73,7 +73,7 @@ describe("Feishu persistent dedupe", () => { await expect(hasProcessedFeishuMessage("msg-3", "account-a")).resolves.toBe(false); }); - it("imports legacy JSON dedupe entries before checking plugin state", async () => { + it("ignores legacy JSON dedupe files at runtime", async () => { vi.useFakeTimers(); vi.setSystemTime(2_000); const legacyPath = path.join(tempDir as string, "feishu", "dedup", "account-a.json"); @@ -87,8 +87,8 @@ describe("Feishu persistent dedupe", () => { "utf8", ); - await expect(hasProcessedFeishuMessage("msg-legacy", "account-a")).resolves.toBe(true); - await expect(tryRecordMessagePersistent("msg-legacy", "account-a")).resolves.toBe(false); + await expect(hasProcessedFeishuMessage("msg-legacy", "account-a")).resolves.toBe(false); + await expect(tryRecordMessagePersistent("msg-legacy", "account-a")).resolves.toBe(true); await expect(hasProcessedFeishuMessage("msg-expired", "account-a")).resolves.toBe(false); }); }); diff --git a/extensions/feishu/src/dedup.ts b/extensions/feishu/src/dedup.ts index 71c82f94a537..dc9bf1486343 100644 --- a/extensions/feishu/src/dedup.ts +++ b/extensions/feishu/src/dedup.ts @@ -1,7 +1,4 @@ import { createHash } from "node:crypto"; -import os from "node:os"; -import path from "node:path"; -import { loadJsonFile } from "openclaw/plugin-sdk/json-store"; import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; import { releaseFeishuMessageProcessing, @@ -20,11 +17,8 @@ type FeishuDedupStoreEntry = { }; const memory = new Map(); -const importedLegacyNamespaces = new Set(); const cachedDedupStores = new Map>(); -type LegacyDedupeData = Record; - function normalizeMessageId(messageId: string | undefined | null): string | null { const trimmed = messageId?.trim(); return trimmed ? trimmed : null; @@ -34,22 +28,6 @@ function normalizeNamespace(namespace?: string): string { return namespace?.trim() || "global"; } -function resolveLegacyStateDir(env: NodeJS.ProcessEnv = process.env): string { - const stateOverride = env.OPENCLAW_STATE_DIR?.trim(); - if (stateOverride) { - return stateOverride; - } - if (env.VITEST || env.NODE_ENV === "test") { - return path.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-")); - } - return path.join(os.homedir(), ".openclaw"); -} - -function resolveLegacyNamespaceFilePath(namespace: string): string { - const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_"); - return path.join(resolveLegacyStateDir(), "feishu", "dedup", `${safe}.json`); -} - function pluginStateNamespace(namespace: string): string { return `dedup.${namespace.replace(/[^a-zA-Z0-9_-]/g, "_")}`; } @@ -116,52 +94,6 @@ function hasMemory(namespace: string, messageId: string, now = Date.now()): bool return false; } -function sanitizeLegacyDedupeData(value: unknown): LegacyDedupeData { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return {}; - } - const out: LegacyDedupeData = {}; - for (const [key, seenAt] of Object.entries(value as Record)) { - if (typeof seenAt === "number" && Number.isFinite(seenAt) && seenAt > 0) { - out[key] = seenAt; - } - } - return out; -} - -function importLegacyDedupNamespace( - namespace: string, - now = Date.now(), - log?: (...args: unknown[]) => void, -): void { - if (importedLegacyNamespaces.has(namespace)) { - return; - } - - try { - const data = sanitizeLegacyDedupeData(loadJsonFile(resolveLegacyNamespaceFilePath(namespace))); - const store = openDedupStore(namespace); - for (const [messageId, seenAt] of Object.entries(data)) { - if (!isRecent(seenAt, now)) { - continue; - } - const key = dedupeStoreKey(namespace, messageId); - if (store.lookup(key) != null) { - continue; - } - store.register( - key, - { namespace, messageId, seenAt }, - { ttlMs: Math.max(1, DEDUP_TTL_MS - (now - seenAt)) }, - ); - } - importedLegacyNamespaces.add(namespace); - } catch (error) { - importedLegacyNamespaces.delete(namespace); - log?.(`feishu-dedup: legacy state import failed: ${String(error)}`); - } -} - export { releaseFeishuMessageProcessing, tryBeginFeishuMessageProcessing }; export async function claimUnprocessedFeishuMessage(params: { @@ -259,7 +191,6 @@ export async function tryRecordMessagePersistent( return true; } const now = Date.now(); - importLegacyDedupNamespace(normalizedNamespace, now, log); if (hasMemory(normalizedNamespace, normalizedMessageId, now)) { return false; } @@ -318,7 +249,6 @@ async function hasRecordedMessagePersistent( return false; } const now = Date.now(); - importLegacyDedupNamespace(normalizedNamespace, now, log); if (hasMemory(normalizedNamespace, normalizedMessageId, now)) { return true; } @@ -337,7 +267,7 @@ async function hasRecordedMessagePersistent( } } -export async function warmupDedupFromDisk( +export async function warmupDedupFromPluginState( namespace: string, log?: (...args: unknown[]) => void, ): Promise { @@ -345,7 +275,6 @@ export async function warmupDedupFromDisk( try { let loaded = 0; const now = Date.now(); - importLegacyDedupNamespace(normalizedNamespace, now, log); for (const entry of openDedupStore(normalizedNamespace).entries()) { if (entry.value.namespace !== normalizedNamespace || !isRecent(entry.value.seenAt, now)) { continue; @@ -363,7 +292,6 @@ export async function warmupDedupFromDisk( export const testingHooks = { resetFeishuDedupForTests() { memory.clear(); - importedLegacyNamespaces.clear(); for (const store of cachedDedupStores.values()) { store.clear(); } diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index e26bcafd54a5..927cae0665cf 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -14,7 +14,7 @@ import { isRecord, readString } from "./comment-shared.js"; import { hasProcessedFeishuMessage, recordProcessedFeishuMessage, - warmupDedupFromDisk, + warmupDedupFromPluginState, } from "./dedup.js"; import { applyBotIdentityState, startBotIdentityRecovery } from "./monitor.bot-identity.js"; import { createFeishuBotMenuHandler } from "./monitor.bot-menu-handler.js"; @@ -469,9 +469,9 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams): throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`); } - const warmupCount = await warmupDedupFromDisk(accountId, log); + const warmupCount = await warmupDedupFromPluginState(accountId, log); if (warmupCount > 0) { - log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`); + log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from plugin state`); } let threadBindingManager: ReturnType | null | undefined;