@openclaw/discord 2026.5.2-beta.1 → 2026.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/discord",
|
|
3
|
-
"version": "2026.5.2
|
|
3
|
+
"version": "2026.5.2",
|
|
4
4
|
"description": "OpenClaw Discord channel plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"openclaw": "workspace:*"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"openclaw": ">=2026.5.2
|
|
24
|
+
"openclaw": ">=2026.5.2"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"openclaw": {
|
|
@@ -61,13 +61,14 @@
|
|
|
61
61
|
"install": {
|
|
62
62
|
"npmSpec": "@openclaw/discord",
|
|
63
63
|
"defaultChoice": "npm",
|
|
64
|
-
"minHostVersion": ">=2026.4.10"
|
|
64
|
+
"minHostVersion": ">=2026.4.10",
|
|
65
|
+
"allowInvalidConfigRecovery": true
|
|
65
66
|
},
|
|
66
67
|
"compat": {
|
|
67
|
-
"pluginApi": ">=2026.5.2
|
|
68
|
+
"pluginApi": ">=2026.5.2"
|
|
68
69
|
},
|
|
69
70
|
"build": {
|
|
70
|
-
"openclawVersion": "2026.5.2
|
|
71
|
+
"openclawVersion": "2026.5.2"
|
|
71
72
|
},
|
|
72
73
|
"release": {
|
|
73
74
|
"publishToClawHub": true,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { ApplicationCommandType, ComponentType, Routes } from "discord-api-types/v10";
|
|
2
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
6
|
import { Client, ComponentRegistry, type AnyListener } from "./client.js";
|
|
@@ -274,6 +277,35 @@ describe("Client.deployCommands", () => {
|
|
|
274
277
|
expect(post).toHaveBeenCalledTimes(1);
|
|
275
278
|
});
|
|
276
279
|
|
|
280
|
+
it("skips unchanged command deploys across client restarts using the hash store", async () => {
|
|
281
|
+
const hashStorePath = path.join(
|
|
282
|
+
await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-discord-command-deploy-")),
|
|
283
|
+
"hashes.json",
|
|
284
|
+
);
|
|
285
|
+
const first = createInternalTestClient([createTestCommand({ name: "one" })], {
|
|
286
|
+
commandDeployHashStorePath: hashStorePath,
|
|
287
|
+
});
|
|
288
|
+
const firstGet = vi.fn(async () => []);
|
|
289
|
+
const firstPost = vi.fn(async () => undefined);
|
|
290
|
+
attachRestMock(first, { get: firstGet, post: firstPost });
|
|
291
|
+
|
|
292
|
+
await first.deployCommands({ mode: "reconcile" });
|
|
293
|
+
|
|
294
|
+
const second = createInternalTestClient([createTestCommand({ name: "one" })], {
|
|
295
|
+
commandDeployHashStorePath: hashStorePath,
|
|
296
|
+
});
|
|
297
|
+
const secondGet = vi.fn(async () => []);
|
|
298
|
+
const secondPost = vi.fn(async () => undefined);
|
|
299
|
+
attachRestMock(second, { get: secondGet, post: secondPost });
|
|
300
|
+
|
|
301
|
+
await second.deployCommands({ mode: "reconcile" });
|
|
302
|
+
|
|
303
|
+
expect(firstGet).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(firstPost).toHaveBeenCalledTimes(1);
|
|
305
|
+
expect(secondGet).not.toHaveBeenCalled();
|
|
306
|
+
expect(secondPost).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
277
309
|
it("caches REST object fetches briefly and invalidates from gateway updates", async () => {
|
|
278
310
|
const client = createInternalTestClient();
|
|
279
311
|
const get = vi.fn(async () => ({ id: "c1", type: 0, name: "general" }));
|
package/src/internal/client.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface ClientOptions {
|
|
|
44
44
|
disableDeployRoute?: boolean;
|
|
45
45
|
disableInteractionsRoute?: boolean;
|
|
46
46
|
disableEventsRoute?: boolean;
|
|
47
|
+
commandDeployHashStorePath?: string;
|
|
47
48
|
devGuilds?: string[];
|
|
48
49
|
eventQueue?: DiscordEventQueueOptions;
|
|
49
50
|
restCacheTtlMs?: number;
|
|
@@ -205,6 +206,7 @@ export class Client {
|
|
|
205
206
|
clientId: this.options.clientId,
|
|
206
207
|
commands: this.commands,
|
|
207
208
|
devGuilds: this.options.devGuilds,
|
|
209
|
+
hashStorePath: this.options.commandDeployHashStorePath,
|
|
208
210
|
rest: () => this.rest,
|
|
209
211
|
});
|
|
210
212
|
for (const component of handlers.components ?? []) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { ApplicationCommandType, type APIApplicationCommand } from "discord-api-types/v10";
|
|
3
5
|
import {
|
|
4
6
|
createApplicationCommand,
|
|
@@ -20,12 +22,14 @@ type SerializedCommand = ReturnType<BaseCommand["serialize"]>;
|
|
|
20
22
|
|
|
21
23
|
export class DiscordCommandDeployer {
|
|
22
24
|
private readonly hashes = new Map<string, string>();
|
|
25
|
+
private hashesLoaded = false;
|
|
23
26
|
|
|
24
27
|
constructor(
|
|
25
28
|
private readonly params: {
|
|
26
29
|
clientId: string;
|
|
27
30
|
commands: BaseCommand[];
|
|
28
31
|
devGuilds?: string[];
|
|
32
|
+
hashStorePath?: string;
|
|
29
33
|
rest: () => RequestClient;
|
|
30
34
|
},
|
|
31
35
|
) {}
|
|
@@ -124,11 +128,67 @@ export class DiscordCommandDeployer {
|
|
|
124
128
|
options: { force?: boolean },
|
|
125
129
|
): Promise<void> {
|
|
126
130
|
const hash = stableCommandSetHash(commands);
|
|
131
|
+
await this.loadPersistedHashes();
|
|
127
132
|
if (!options.force && this.hashes.get(key) === hash) {
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
await deploy();
|
|
131
136
|
this.hashes.set(key, hash);
|
|
137
|
+
await this.persistHashes();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async loadPersistedHashes(): Promise<void> {
|
|
141
|
+
if (this.hashesLoaded) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.hashesLoaded = true;
|
|
145
|
+
const storePath = this.params.hashStorePath;
|
|
146
|
+
if (!storePath) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const raw = await fs.readFile(storePath, "utf8");
|
|
151
|
+
const parsed = JSON.parse(raw) as { hashes?: unknown };
|
|
152
|
+
if (!parsed.hashes || typeof parsed.hashes !== "object") {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const [key, value] of Object.entries(parsed.hashes)) {
|
|
156
|
+
if (typeof value === "string" && key.trim() && value.trim()) {
|
|
157
|
+
this.hashes.set(key, value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Best-effort cache only. A corrupt or missing file should never block startup.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async persistHashes(): Promise<void> {
|
|
166
|
+
const storePath = this.params.hashStorePath;
|
|
167
|
+
if (!storePath) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
|
172
|
+
const tmpPath = `${storePath}.${process.pid}.${Date.now()}.tmp`;
|
|
173
|
+
await fs.writeFile(
|
|
174
|
+
tmpPath,
|
|
175
|
+
`${JSON.stringify(
|
|
176
|
+
{
|
|
177
|
+
version: 1,
|
|
178
|
+
updatedAt: new Date().toISOString(),
|
|
179
|
+
hashes: Object.fromEntries(
|
|
180
|
+
[...this.hashes.entries()].toSorted(([left], [right]) => left.localeCompare(right)),
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
null,
|
|
184
|
+
2,
|
|
185
|
+
)}\n`,
|
|
186
|
+
"utf8",
|
|
187
|
+
);
|
|
188
|
+
await fs.rename(tmpPath, storePath);
|
|
189
|
+
} catch {
|
|
190
|
+
// The cache is only an optimization to avoid redundant Discord writes.
|
|
191
|
+
}
|
|
132
192
|
}
|
|
133
193
|
|
|
134
194
|
private get rest(): RequestClient {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentType, InteractionType } from "discord-api-types/v10";
|
|
2
2
|
import { vi, type Mock } from "vitest";
|
|
3
|
-
import { Client } from "./client.js";
|
|
3
|
+
import { Client, type ClientOptions } from "./client.js";
|
|
4
4
|
import type { BaseCommand } from "./commands.js";
|
|
5
5
|
import type { RawInteraction } from "./interactions.js";
|
|
6
6
|
import type { QueuedRequest, RequestClient, RequestData } from "./rest.js";
|
|
@@ -58,13 +58,17 @@ export function createAbortableFetchMock() {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function createInternalTestClient(
|
|
61
|
+
export function createInternalTestClient(
|
|
62
|
+
commands: BaseCommand[] = [],
|
|
63
|
+
options?: Partial<ClientOptions>,
|
|
64
|
+
): Client {
|
|
62
65
|
return new Client(
|
|
63
66
|
{
|
|
64
67
|
baseUrl: "http://localhost",
|
|
65
68
|
clientId: "app1",
|
|
66
69
|
publicKey: "public",
|
|
67
70
|
token: "token",
|
|
71
|
+
...options,
|
|
68
72
|
},
|
|
69
73
|
{ commands },
|
|
70
74
|
);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
2
3
|
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
|
3
4
|
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
|
4
5
|
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
6
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
5
7
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
6
8
|
import {
|
|
7
9
|
Client,
|
|
@@ -136,6 +138,11 @@ export async function createDiscordMonitorClient(params: {
|
|
|
136
138
|
publicKey: "a",
|
|
137
139
|
token: params.token,
|
|
138
140
|
autoDeploy: false,
|
|
141
|
+
commandDeployHashStorePath: path.join(
|
|
142
|
+
resolveStateDir(process.env),
|
|
143
|
+
"discord",
|
|
144
|
+
"command-deploy-cache.json",
|
|
145
|
+
),
|
|
139
146
|
requestOptions: {
|
|
140
147
|
timeout: DISCORD_REST_TIMEOUT_MS,
|
|
141
148
|
runtimeProfile: "persistent",
|