





















@@ -1,12 +1,36 @@
11import { resolveGlobalMap } from "openclaw/plugin-sdk/global-singleton";
22import type { DiscordComponentEntry, DiscordModalEntry } from "./components.js";
3+import { getOptionalDiscordRuntime } from "./runtime.js";
3445const DEFAULT_COMPONENT_TTL_MS = 30 * 60 * 1000;
6+const PERSISTENT_COMPONENT_NAMESPACE = "discord.components";
7+const PERSISTENT_MODAL_NAMESPACE = "discord.modals";
8+const PERSISTENT_COMPONENT_MAX_ENTRIES = 500;
9+const PERSISTENT_MODAL_MAX_ENTRIES = 500;
510const DISCORD_COMPONENT_ENTRIES_KEY = Symbol.for("openclaw.discord.componentEntries");
611const DISCORD_MODAL_ENTRIES_KEY = Symbol.for("openclaw.discord.modalEntries");
71213+type PersistedDiscordRegistryEntry<T extends { id: string }> = {
14+version: 1;
15+entry: T;
16+};
17+18+type DiscordPersistentStore<T> = {
19+register(key: string, value: T, opts?: { ttlMs?: number }): Promise<void>;
20+lookup(key: string): Promise<T | undefined>;
21+consume(key: string): Promise<T | undefined>;
22+delete(key: string): Promise<boolean>;
23+};
24+25+type DiscordRegistryStore<T extends { id: string }> = DiscordPersistentStore<
26+PersistedDiscordRegistryEntry<T>
27+>;
28+829let componentEntries: Map<string, DiscordComponentEntry> | undefined;
930let modalEntries: Map<string, DiscordModalEntry> | undefined;
31+let persistentComponentStore: DiscordRegistryStore<DiscordComponentEntry> | undefined;
32+let persistentModalStore: DiscordRegistryStore<DiscordModalEntry> | undefined;
33+let persistentRegistryDisabled = false;
10341135function getComponentEntries(): Map<string, DiscordComponentEntry> {
1236componentEntries ??= resolveGlobalMap<string, DiscordComponentEntry>(
@@ -20,6 +44,75 @@ function getModalEntries(): Map<string, DiscordModalEntry> {
2044return modalEntries;
2145}
224647+function reportPersistentComponentRegistryError(error: unknown): void {
48+try {
49+getOptionalDiscordRuntime()
50+?.logging.getChildLogger({ plugin: "discord", feature: "component-registry-state" })
51+.warn("Discord persistent component registry state failed", { error: String(error) });
52+} catch {
53+// Best effort only: persistent state must never break Discord interactions.
54+}
55+}
56+57+function disablePersistentComponentRegistry(error: unknown): void {
58+persistentRegistryDisabled = true;
59+persistentComponentStore = undefined;
60+persistentModalStore = undefined;
61+reportPersistentComponentRegistryError(error);
62+}
63+64+function getPersistentComponentStore(): DiscordRegistryStore<DiscordComponentEntry> | undefined {
65+if (persistentRegistryDisabled) {
66+return undefined;
67+}
68+if (persistentComponentStore) {
69+return persistentComponentStore;
70+}
71+const runtime = getOptionalDiscordRuntime();
72+if (!runtime) {
73+return undefined;
74+}
75+try {
76+persistentComponentStore = runtime.state.openKeyedStore<
77+PersistedDiscordRegistryEntry<DiscordComponentEntry>
78+>({
79+namespace: PERSISTENT_COMPONENT_NAMESPACE,
80+maxEntries: PERSISTENT_COMPONENT_MAX_ENTRIES,
81+defaultTtlMs: DEFAULT_COMPONENT_TTL_MS,
82+});
83+return persistentComponentStore;
84+} catch (error) {
85+disablePersistentComponentRegistry(error);
86+return undefined;
87+}
88+}
89+90+function getPersistentModalStore(): DiscordRegistryStore<DiscordModalEntry> | undefined {
91+if (persistentRegistryDisabled) {
92+return undefined;
93+}
94+if (persistentModalStore) {
95+return persistentModalStore;
96+}
97+const runtime = getOptionalDiscordRuntime();
98+if (!runtime) {
99+return undefined;
100+}
101+try {
102+persistentModalStore = runtime.state.openKeyedStore<
103+PersistedDiscordRegistryEntry<DiscordModalEntry>
104+>({
105+namespace: PERSISTENT_MODAL_NAMESPACE,
106+maxEntries: PERSISTENT_MODAL_MAX_ENTRIES,
107+defaultTtlMs: DEFAULT_COMPONENT_TTL_MS,
108+});
109+return persistentModalStore;
110+} catch (error) {
111+disablePersistentComponentRegistry(error);
112+return undefined;
113+}
114+}
115+23116function isExpired(entry: { expiresAt?: number }, now: number) {
24117return typeof entry.expiresAt === "number" && entry.expiresAt <= now;
25118}
@@ -40,15 +133,18 @@ function registerEntries<
40133entries: T[],
41134store: Map<string, T>,
42135params: { now: number; ttlMs: number; messageId?: string },
43-): void {
136+): T[] {
137+const normalizedEntries: T[] = [];
44138for (const entry of entries) {
45139const normalized = normalizeEntryTimestamps(
46140{ ...entry, messageId: params.messageId ?? entry.messageId },
47141params.now,
48142params.ttlMs,
49143);
50144store.set(entry.id, normalized);
145+normalizedEntries.push(normalized);
51146}
147+return normalizedEntries;
52148}
5314954150function resolveEntry<T extends { expiresAt?: number }>(
@@ -70,6 +166,81 @@ function resolveEntry<T extends { expiresAt?: number }>(
70166return entry;
71167}
72168169+function readPersistedRegistryEntry<T extends { id: string }>(
170+persisted: PersistedDiscordRegistryEntry<T> | undefined,
171+): T | null {
172+if (persisted?.version !== 1 || typeof persisted.entry?.id !== "string") {
173+return null;
174+}
175+return persisted.entry;
176+}
177+178+function registerPersistentRegistryEntries<T extends { id: string }>(params: {
179+entries: T[];
180+ttlMs: number;
181+openStore: () => DiscordRegistryStore<T> | undefined;
182+}): void {
183+if (params.entries.length === 0) {
184+return;
185+}
186+const store = params.openStore();
187+if (!store) {
188+return;
189+}
190+for (const entry of params.entries) {
191+void store
192+.register(entry.id, { version: 1, entry }, { ttlMs: params.ttlMs })
193+.catch(disablePersistentComponentRegistry);
194+}
195+}
196+197+function registerPersistentEntries(params: {
198+entries: DiscordComponentEntry[];
199+modals: DiscordModalEntry[];
200+ttlMs: number;
201+}): void {
202+registerPersistentRegistryEntries({
203+entries: params.entries,
204+ttlMs: params.ttlMs,
205+openStore: getPersistentComponentStore,
206+});
207+registerPersistentRegistryEntries({
208+entries: params.modals,
209+ttlMs: params.ttlMs,
210+openStore: getPersistentModalStore,
211+});
212+}
213+214+function deletePersistentEntry<T extends { id: string }>(params: {
215+id: string;
216+openStore: () => DiscordRegistryStore<T> | undefined;
217+}): void {
218+const store = params.openStore();
219+if (!store) {
220+return;
221+}
222+void store.delete(params.id).catch(disablePersistentComponentRegistry);
223+}
224+225+async function resolvePersistentRegistryEntry<T extends { id: string }>(params: {
226+id: string;
227+consume?: boolean;
228+openStore: () => DiscordRegistryStore<T> | undefined;
229+}): Promise<T | null> {
230+const store = params.openStore();
231+if (!store) {
232+return null;
233+}
234+try {
235+const value =
236+params.consume === false ? await store.lookup(params.id) : await store.consume(params.id);
237+return readPersistedRegistryEntry(value);
238+} catch (error) {
239+disablePersistentComponentRegistry(error);
240+return null;
241+}
242+}
243+73244export function registerDiscordComponentEntries(params: {
74245entries: DiscordComponentEntry[];
75246modals: DiscordModalEntry[];
@@ -78,12 +249,21 @@ export function registerDiscordComponentEntries(params: {
78249}): void {
79250const now = Date.now();
80251const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
81-registerEntries(params.entries, getComponentEntries(), {
252+const normalizedEntries = registerEntries(params.entries, getComponentEntries(), {
253+ now,
254+ ttlMs,
255+messageId: params.messageId,
256+});
257+const normalizedModals = registerEntries(params.modals, getModalEntries(), {
82258 now,
83259 ttlMs,
84260messageId: params.messageId,
85261});
86-registerEntries(params.modals, getModalEntries(), { now, ttlMs, messageId: params.messageId });
262+registerPersistentEntries({
263+entries: normalizedEntries,
264+modals: normalizedModals,
265+ ttlMs,
266+});
87267}
8826889269export function resolveDiscordComponentEntry(params: {
@@ -93,14 +273,51 @@ export function resolveDiscordComponentEntry(params: {
93273return resolveEntry(getComponentEntries(), params);
94274}
95275276+export async function resolveDiscordComponentEntryWithPersistence(params: {
277+id: string;
278+consume?: boolean;
279+}): Promise<DiscordComponentEntry | null> {
280+const inMemory = resolveDiscordComponentEntry(params);
281+if (inMemory) {
282+if (params.consume !== false) {
283+deletePersistentEntry({ ...params, openStore: getPersistentComponentStore });
284+}
285+return inMemory;
286+}
287+return await resolvePersistentRegistryEntry({
288+ ...params,
289+openStore: getPersistentComponentStore,
290+});
291+}
292+96293export function resolveDiscordModalEntry(params: {
97294id: string;
98295consume?: boolean;
99296}): DiscordModalEntry | null {
100297return resolveEntry(getModalEntries(), params);
101298}
102299300+export async function resolveDiscordModalEntryWithPersistence(params: {
301+id: string;
302+consume?: boolean;
303+}): Promise<DiscordModalEntry | null> {
304+const inMemory = resolveDiscordModalEntry(params);
305+if (inMemory) {
306+if (params.consume !== false) {
307+deletePersistentEntry({ ...params, openStore: getPersistentModalStore });
308+}
309+return inMemory;
310+}
311+return await resolvePersistentRegistryEntry({
312+ ...params,
313+openStore: getPersistentModalStore,
314+});
315+}
316+103317export function clearDiscordComponentEntries(): void {
104318getComponentEntries().clear();
105319getModalEntries().clear();
320+persistentComponentStore = undefined;
321+persistentModalStore = undefined;
322+persistentRegistryDisabled = false;
106323}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。