mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-05 22:33:34 +00:00
* refactor(plugin-sdk): add createPersistentDedupeCache and migrate channel presence caches * refactor(matrix): adopt SDK approval reaction target store * refactor(plugin-sdk): share doctor legacy-state migration fs helpers * refactor(memory-core): dedupe qmd cache entry envelope validation * chore(plugin-sdk): pin surface budgets for shared dedupe and doctor helpers * test(matrix): use future approval expiry fixtures for reaction targets * test(matrix): use future approval expiry fixtures for reaction targets
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
// Slack plugin module implements sent thread cache behavior.
|
|
import { createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
|
import { getOptionalSlackRuntime } from "./runtime.js";
|
|
|
|
/**
|
|
* Cache of Slack threads the bot has participated in.
|
|
* Used to auto-respond in threads without requiring @mention after the first reply.
|
|
*/
|
|
|
|
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
const MAX_ENTRIES = 5000;
|
|
const PERSISTENT_MAX_ENTRIES = 1000;
|
|
const PERSISTENT_NAMESPACE = "slack.thread-participation";
|
|
|
|
type SlackThreadParticipationRecord = {
|
|
agentId?: string;
|
|
repliedAt: number;
|
|
};
|
|
|
|
/**
|
|
* Keep Slack thread participation shared across bundled chunks so thread
|
|
* auto-reply gating does not diverge between prepare/dispatch call paths.
|
|
*/
|
|
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
|
|
const threadParticipation = createPersistentDedupeCache<SlackThreadParticipationRecord>({
|
|
globalKey: SLACK_THREAD_PARTICIPATION_KEY,
|
|
ttlMs: TTL_MS,
|
|
maxSize: MAX_ENTRIES,
|
|
persistent: {
|
|
namespace: PERSISTENT_NAMESPACE,
|
|
maxEntries: PERSISTENT_MAX_ENTRIES,
|
|
openStore: (options) => getOptionalSlackRuntime()?.state.openKeyedStore(options),
|
|
logError: (error) => {
|
|
try {
|
|
getOptionalSlackRuntime()
|
|
?.logging.getChildLogger({ plugin: "slack", feature: "thread-participation-state" })
|
|
.warn("Slack persistent thread participation state failed", { error: String(error) });
|
|
} catch {
|
|
// Best effort only: persistent state must never break Slack message handling.
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
function makeKey(accountId: string, channelId: string, threadTs: string): string {
|
|
return `${accountId}:${channelId}:${threadTs}`;
|
|
}
|
|
|
|
export function recordSlackThreadParticipation(
|
|
accountId: string,
|
|
channelId: string,
|
|
threadTs: string,
|
|
opts?: { agentId?: string },
|
|
): void {
|
|
if (!accountId || !channelId || !threadTs) {
|
|
return;
|
|
}
|
|
void threadParticipation.register(makeKey(accountId, channelId, threadTs), {
|
|
// Stored for future per-agent thread routing; current reads only need presence.
|
|
...(opts?.agentId ? { agentId: opts.agentId } : {}),
|
|
repliedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
export function hasSlackThreadParticipation(
|
|
accountId: string,
|
|
channelId: string,
|
|
threadTs: string,
|
|
): boolean {
|
|
if (!accountId || !channelId || !threadTs) {
|
|
return false;
|
|
}
|
|
return threadParticipation.peek(makeKey(accountId, channelId, threadTs));
|
|
}
|
|
|
|
export async function hasSlackThreadParticipationWithPersistence(params: {
|
|
accountId: string;
|
|
channelId: string;
|
|
threadTs: string;
|
|
}): Promise<boolean> {
|
|
if (!params.accountId || !params.channelId || !params.threadTs) {
|
|
return false;
|
|
}
|
|
return await threadParticipation.lookup(
|
|
makeKey(params.accountId, params.channelId, params.threadTs),
|
|
);
|
|
}
|
|
|
|
export function clearSlackThreadParticipationCache(): void {
|
|
threadParticipation.clearForTest();
|
|
}
|