@openclaw/discord 2026.5.1-beta.1 → 2026.5.2-beta.1
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/openclaw.plugin.json +50 -6
- package/package.json +5 -4
- package/src/actions/handle-action.test.ts +121 -0
- package/src/actions/handle-action.ts +49 -9
- package/src/actions/runtime.messaging.send.ts +8 -4
- package/src/actions/runtime.messaging.shared.ts +5 -0
- package/src/actions/runtime.test.ts +32 -1
- package/src/actions/runtime.ts +6 -0
- package/src/channel-actions.test.ts +42 -3
- package/src/channel-actions.ts +5 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.test.ts +8 -0
- package/src/channel.ts +1 -0
- package/src/client.ts +0 -7
- package/src/config-ui-hints.ts +6 -6
- package/src/internal/client.test.ts +111 -0
- package/src/internal/client.ts +63 -1
- package/src/internal/command-deploy.ts +41 -6
- package/src/internal/gateway.test.ts +128 -0
- package/src/internal/gateway.ts +44 -5
- package/src/internal/interaction-dispatch.ts +33 -1
- package/src/internal/interactions.test.ts +72 -0
- package/src/internal/interactions.ts +41 -0
- package/src/internal/rest-scheduler.ts +188 -43
- package/src/internal/rest.test.ts +236 -0
- package/src/internal/rest.ts +114 -5
- package/src/internal/structures.test.ts +43 -0
- package/src/internal/structures.ts +2 -0
- package/src/monitor/agent-components-context.ts +12 -2
- package/src/monitor/agent-components.dispatch.ts +2 -2
- package/src/monitor/agent-components.types.ts +1 -0
- package/src/monitor/allow-list.test.ts +14 -0
- package/src/monitor/allow-list.ts +10 -0
- package/src/monitor/channel-access.test.ts +99 -0
- package/src/monitor/channel-access.ts +36 -4
- package/src/monitor/message-handler.context.ts +16 -3
- package/src/monitor/message-handler.preflight-channel-context.test.ts +18 -0
- package/src/monitor/message-handler.preflight-channel-context.ts +4 -1
- package/src/monitor/message-handler.preflight-pluralkit.ts +1 -2
- package/src/monitor/message-handler.preflight.test.ts +79 -0
- package/src/monitor/message-handler.preflight.ts +21 -22
- package/src/monitor/message-handler.preflight.types.ts +1 -0
- package/src/monitor/message-handler.process.test.ts +70 -2
- package/src/monitor/message-handler.process.ts +4 -2
- package/src/monitor/message-media.ts +2 -0
- package/src/monitor/message-utils.test.ts +7 -1
- package/src/monitor/native-command-agent-reply.ts +3 -1
- package/src/monitor/native-command-reply.ts +2 -0
- package/src/monitor/native-command.plugin-dispatch.test.ts +82 -0
- package/src/monitor/native-command.ts +2 -1
- package/src/monitor/provider.lifecycle.test.ts +56 -1
- package/src/monitor/provider.lifecycle.ts +7 -0
- package/src/monitor/thread-bindings.discord-api.ts +6 -14
- package/src/proxy-request-client.ts +1 -34
- package/src/send.components.ts +2 -4
- package/src/send.outbound.ts +4 -5
- package/src/send.sends-basic-channel-messages.test.ts +22 -0
- package/src/send.shared.ts +3 -1
- package/src/setup-core.ts +37 -5
- package/src/setup-surface.test.ts +41 -0
- package/src/shared.test.ts +6 -0
- package/src/subagent-hooks.test.ts +91 -38
- package/src/subagent-hooks.ts +28 -29
package/openclaw.plugin.json
CHANGED
|
@@ -193,6 +193,16 @@
|
|
|
193
193
|
"dangerouslyAllowNameMatching": {
|
|
194
194
|
"type": "boolean"
|
|
195
195
|
},
|
|
196
|
+
"mentionAliases": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"propertyNames": {
|
|
199
|
+
"type": "string"
|
|
200
|
+
},
|
|
201
|
+
"additionalProperties": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"pattern": "^\\d+$"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
196
206
|
"groupPolicy": {
|
|
197
207
|
"default": "allowlist",
|
|
198
208
|
"type": "string",
|
|
@@ -857,6 +867,16 @@
|
|
|
857
867
|
"type": "number",
|
|
858
868
|
"minimum": 0
|
|
859
869
|
},
|
|
870
|
+
"spawnSessions": {
|
|
871
|
+
"type": "boolean"
|
|
872
|
+
},
|
|
873
|
+
"defaultSpawnContext": {
|
|
874
|
+
"type": "string",
|
|
875
|
+
"enum": [
|
|
876
|
+
"isolated",
|
|
877
|
+
"fork"
|
|
878
|
+
]
|
|
879
|
+
},
|
|
860
880
|
"spawnSubagentSessions": {
|
|
861
881
|
"type": "boolean"
|
|
862
882
|
},
|
|
@@ -1698,6 +1718,16 @@
|
|
|
1698
1718
|
"dangerouslyAllowNameMatching": {
|
|
1699
1719
|
"type": "boolean"
|
|
1700
1720
|
},
|
|
1721
|
+
"mentionAliases": {
|
|
1722
|
+
"type": "object",
|
|
1723
|
+
"propertyNames": {
|
|
1724
|
+
"type": "string"
|
|
1725
|
+
},
|
|
1726
|
+
"additionalProperties": {
|
|
1727
|
+
"type": "string",
|
|
1728
|
+
"pattern": "^\\d+$"
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1701
1731
|
"groupPolicy": {
|
|
1702
1732
|
"default": "allowlist",
|
|
1703
1733
|
"type": "string",
|
|
@@ -2362,6 +2392,16 @@
|
|
|
2362
2392
|
"type": "number",
|
|
2363
2393
|
"minimum": 0
|
|
2364
2394
|
},
|
|
2395
|
+
"spawnSessions": {
|
|
2396
|
+
"type": "boolean"
|
|
2397
|
+
},
|
|
2398
|
+
"defaultSpawnContext": {
|
|
2399
|
+
"type": "string",
|
|
2400
|
+
"enum": [
|
|
2401
|
+
"isolated",
|
|
2402
|
+
"fork"
|
|
2403
|
+
]
|
|
2404
|
+
},
|
|
2365
2405
|
"spawnSubagentSessions": {
|
|
2366
2406
|
"type": "boolean"
|
|
2367
2407
|
},
|
|
@@ -3155,13 +3195,13 @@
|
|
|
3155
3195
|
"label": "Discord Thread Binding Max Age (hours)",
|
|
3156
3196
|
"help": "Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."
|
|
3157
3197
|
},
|
|
3158
|
-
"threadBindings.
|
|
3159
|
-
"label": "Discord Thread-Bound
|
|
3160
|
-
"help": "Allow
|
|
3198
|
+
"threadBindings.spawnSessions": {
|
|
3199
|
+
"label": "Discord Thread-Bound Session Spawn",
|
|
3200
|
+
"help": "Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel."
|
|
3161
3201
|
},
|
|
3162
|
-
"threadBindings.
|
|
3163
|
-
"label": "Discord Thread
|
|
3164
|
-
"help": "
|
|
3202
|
+
"threadBindings.defaultSpawnContext": {
|
|
3203
|
+
"label": "Discord Thread Spawn Context",
|
|
3204
|
+
"help": "Default native subagent context for thread-bound spawns. \"fork\" starts from the requester transcript; \"isolated\" starts clean. Default: \"fork\"."
|
|
3165
3205
|
},
|
|
3166
3206
|
"ui.components.accentColor": {
|
|
3167
3207
|
"label": "Discord Component Accent Color",
|
|
@@ -3275,6 +3315,10 @@
|
|
|
3275
3315
|
"label": "Discord Allow Bot Messages",
|
|
3276
3316
|
"help": "Allow bot-authored messages to trigger Discord replies (default: false). Set \"mentions\" to only accept bot messages that mention the bot."
|
|
3277
3317
|
},
|
|
3318
|
+
"mentionAliases": {
|
|
3319
|
+
"label": "Discord Mention Aliases",
|
|
3320
|
+
"help": "Map outbound @handle text to stable Discord user IDs before sending. Set per account via channels.discord.accounts.<id>.mentionAliases."
|
|
3321
|
+
},
|
|
3278
3322
|
"token": {
|
|
3279
3323
|
"label": "Discord Bot Token",
|
|
3280
3324
|
"help": "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/discord",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.2-beta.1",
|
|
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.
|
|
24
|
+
"openclaw": ">=2026.5.2-beta.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"openclaw": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"blurb": "very well supported right now.",
|
|
44
44
|
"systemImage": "bubble.left.and.bubble.right",
|
|
45
45
|
"markdownCapable": true,
|
|
46
|
+
"preferSessionLookupForAnnounceTarget": true,
|
|
46
47
|
"commands": {
|
|
47
48
|
"nativeCommandsAutoEnabled": true,
|
|
48
49
|
"nativeSkillsAutoEnabled": true
|
|
@@ -63,10 +64,10 @@
|
|
|
63
64
|
"minHostVersion": ">=2026.4.10"
|
|
64
65
|
},
|
|
65
66
|
"compat": {
|
|
66
|
-
"pluginApi": ">=2026.
|
|
67
|
+
"pluginApi": ">=2026.5.2-beta.1"
|
|
67
68
|
},
|
|
68
69
|
"build": {
|
|
69
|
-
"openclawVersion": "2026.5.
|
|
70
|
+
"openclawVersion": "2026.5.2-beta.1"
|
|
70
71
|
},
|
|
71
72
|
"release": {
|
|
72
73
|
"publishToClawHub": true,
|
|
@@ -125,6 +125,127 @@ describe("handleDiscordMessageAction", () => {
|
|
|
125
125
|
);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it("maps upload-file to Discord sendMessage with media read context", async () => {
|
|
129
|
+
const mediaReadFile = vi.fn(async () => Buffer.from("image"));
|
|
130
|
+
const mediaAccess = {
|
|
131
|
+
localRoots: ["/tmp/agent-root"],
|
|
132
|
+
readFile: mediaReadFile,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
await handleDiscordMessageAction({
|
|
136
|
+
action: "upload-file",
|
|
137
|
+
params: {
|
|
138
|
+
target: "channel:123",
|
|
139
|
+
filePath: "/tmp/agent-root/image.png",
|
|
140
|
+
message: "caption",
|
|
141
|
+
filename: "image.png",
|
|
142
|
+
replyTo: "message-1",
|
|
143
|
+
silent: true,
|
|
144
|
+
__sessionKey: "session-1",
|
|
145
|
+
__agentId: "agent-1",
|
|
146
|
+
},
|
|
147
|
+
cfg: {
|
|
148
|
+
channels: { discord: { token: "tok" } },
|
|
149
|
+
} as OpenClawConfig,
|
|
150
|
+
mediaAccess,
|
|
151
|
+
mediaLocalRoots: ["/tmp/agent-root"],
|
|
152
|
+
mediaReadFile,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(handleDiscordActionMock).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
action: "sendMessage",
|
|
158
|
+
to: "channel:123",
|
|
159
|
+
content: "caption",
|
|
160
|
+
mediaUrl: "/tmp/agent-root/image.png",
|
|
161
|
+
filename: "image.png",
|
|
162
|
+
replyTo: "message-1",
|
|
163
|
+
silent: true,
|
|
164
|
+
__sessionKey: "session-1",
|
|
165
|
+
__agentId: "agent-1",
|
|
166
|
+
}),
|
|
167
|
+
expect.any(Object),
|
|
168
|
+
{
|
|
169
|
+
mediaAccess,
|
|
170
|
+
mediaLocalRoots: ["/tmp/agent-root"],
|
|
171
|
+
mediaReadFile,
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("falls back to Discord toolContext.currentChannelId for upload-file", async () => {
|
|
177
|
+
await handleDiscordMessageAction({
|
|
178
|
+
action: "upload-file",
|
|
179
|
+
params: {
|
|
180
|
+
path: "/tmp/agent-root/image.png",
|
|
181
|
+
},
|
|
182
|
+
cfg: {
|
|
183
|
+
channels: { discord: { token: "tok" } },
|
|
184
|
+
} as OpenClawConfig,
|
|
185
|
+
toolContext: {
|
|
186
|
+
currentChannelProvider: "discord",
|
|
187
|
+
currentChannelId: "channel:123",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(handleDiscordActionMock).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
action: "sendMessage",
|
|
194
|
+
to: "channel:123",
|
|
195
|
+
content: "",
|
|
196
|
+
mediaUrl: "/tmp/agent-root/image.png",
|
|
197
|
+
}),
|
|
198
|
+
expect.any(Object),
|
|
199
|
+
expect.any(Object),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("requires a file path for upload-file", async () => {
|
|
204
|
+
await expect(
|
|
205
|
+
handleDiscordMessageAction({
|
|
206
|
+
action: "upload-file",
|
|
207
|
+
params: {
|
|
208
|
+
to: "channel:123",
|
|
209
|
+
},
|
|
210
|
+
cfg: {
|
|
211
|
+
channels: { discord: { token: "tok" } },
|
|
212
|
+
} as OpenClawConfig,
|
|
213
|
+
}),
|
|
214
|
+
).rejects.toThrow(/upload-file requires filePath, path, or media/i);
|
|
215
|
+
|
|
216
|
+
expect(handleDiscordActionMock).not.toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("forwards top-level components on sends", async () => {
|
|
220
|
+
const components = { blocks: [{ type: "text", text: "Pick one" }] };
|
|
221
|
+
|
|
222
|
+
await handleDiscordMessageAction({
|
|
223
|
+
action: "send",
|
|
224
|
+
params: {
|
|
225
|
+
message: "hello",
|
|
226
|
+
components,
|
|
227
|
+
},
|
|
228
|
+
cfg: {
|
|
229
|
+
channels: { discord: { token: "tok" } },
|
|
230
|
+
} as OpenClawConfig,
|
|
231
|
+
toolContext: {
|
|
232
|
+
currentChannelProvider: "discord",
|
|
233
|
+
currentChannelId: "channel:123",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(handleDiscordActionMock).toHaveBeenCalledWith(
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
action: "sendMessage",
|
|
240
|
+
to: "channel:123",
|
|
241
|
+
content: "hello",
|
|
242
|
+
components,
|
|
243
|
+
}),
|
|
244
|
+
expect.any(Object),
|
|
245
|
+
expect.any(Object),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
128
249
|
it("does not use another provider's current target for Discord sends", async () => {
|
|
129
250
|
await expect(
|
|
130
251
|
handleDiscordMessageAction({
|
|
@@ -66,32 +66,37 @@ export async function handleDiscordMessageAction(
|
|
|
66
66
|
return target;
|
|
67
67
|
};
|
|
68
68
|
const resolveChannelId = () => resolveDiscordChannelId(readTarget());
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const to =
|
|
69
|
+
const readSendTarget = () => {
|
|
70
|
+
const target =
|
|
72
71
|
readStringParam(params, "to") ??
|
|
73
72
|
readStringParam(params, "target") ??
|
|
74
73
|
readCurrentDiscordTarget(ctx.toolContext);
|
|
75
|
-
if (!
|
|
74
|
+
if (!target) {
|
|
76
75
|
throw new Error("Discord channel target is required (use channel:<id>).");
|
|
77
76
|
}
|
|
77
|
+
return target;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (action === "send") {
|
|
81
|
+
const to = readSendTarget();
|
|
78
82
|
const asVoice = readBooleanParam(params, "asVoice") === true;
|
|
79
83
|
const rawComponents =
|
|
84
|
+
params.components ??
|
|
80
85
|
buildDiscordPresentationComponents(normalizeMessagePresentation(params.presentation)) ??
|
|
81
86
|
buildDiscordInteractiveComponents(normalizeInteractiveReply(params.interactive));
|
|
82
87
|
const hasComponents =
|
|
83
88
|
Boolean(rawComponents) &&
|
|
84
89
|
(typeof rawComponents === "function" || typeof rawComponents === "object");
|
|
85
90
|
const components = hasComponents ? rawComponents : undefined;
|
|
86
|
-
const content = readStringParam(params, "message", {
|
|
87
|
-
required: !asVoice && !hasComponents,
|
|
88
|
-
allowEmpty: true,
|
|
89
|
-
});
|
|
90
91
|
// Support media, path, and filePath for media URL
|
|
91
92
|
const mediaUrl =
|
|
92
93
|
readStringParam(params, "media", { trim: false }) ??
|
|
93
94
|
readStringParam(params, "path", { trim: false }) ??
|
|
94
95
|
readStringParam(params, "filePath", { trim: false });
|
|
96
|
+
const content = readStringParam(params, "message", {
|
|
97
|
+
required: !asVoice && !hasComponents && !mediaUrl,
|
|
98
|
+
allowEmpty: true,
|
|
99
|
+
});
|
|
95
100
|
const filename = readStringParam(params, "filename");
|
|
96
101
|
const replyTo = readStringParam(params, "replyTo");
|
|
97
102
|
const rawEmbeds = params.embeds;
|
|
@@ -104,7 +109,7 @@ export async function handleDiscordMessageAction(
|
|
|
104
109
|
action: "sendMessage",
|
|
105
110
|
accountId: accountId ?? undefined,
|
|
106
111
|
to,
|
|
107
|
-
content,
|
|
112
|
+
content: content ?? "",
|
|
108
113
|
mediaUrl: mediaUrl ?? undefined,
|
|
109
114
|
filename: filename ?? undefined,
|
|
110
115
|
replyTo: replyTo ?? undefined,
|
|
@@ -120,6 +125,41 @@ export async function handleDiscordMessageAction(
|
|
|
120
125
|
);
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
if (action === "upload-file") {
|
|
129
|
+
const to = readSendTarget();
|
|
130
|
+
const mediaUrl =
|
|
131
|
+
readStringParam(params, "filePath", { trim: false }) ??
|
|
132
|
+
readStringParam(params, "path", { trim: false }) ??
|
|
133
|
+
readStringParam(params, "media", { trim: false });
|
|
134
|
+
if (!mediaUrl) {
|
|
135
|
+
throw new Error("upload-file requires filePath, path, or media.");
|
|
136
|
+
}
|
|
137
|
+
const content =
|
|
138
|
+
readStringParam(params, "message", { allowEmpty: true }) ??
|
|
139
|
+
readStringParam(params, "content", { allowEmpty: true });
|
|
140
|
+
const filename = readStringParam(params, "filename");
|
|
141
|
+
const replyTo = readStringParam(params, "replyTo");
|
|
142
|
+
const silent = readBooleanParam(params, "silent") === true;
|
|
143
|
+
const sessionKey = readStringParam(params, "__sessionKey");
|
|
144
|
+
const agentId = readStringParam(params, "__agentId");
|
|
145
|
+
return await handleDiscordAction(
|
|
146
|
+
{
|
|
147
|
+
action: "sendMessage",
|
|
148
|
+
accountId: accountId ?? undefined,
|
|
149
|
+
to,
|
|
150
|
+
content: content ?? "",
|
|
151
|
+
mediaUrl,
|
|
152
|
+
filename: filename ?? undefined,
|
|
153
|
+
replyTo: replyTo ?? undefined,
|
|
154
|
+
silent,
|
|
155
|
+
__sessionKey: sessionKey ?? undefined,
|
|
156
|
+
__agentId: agentId ?? undefined,
|
|
157
|
+
},
|
|
158
|
+
cfg,
|
|
159
|
+
actionOptions,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
123
163
|
if (action === "poll") {
|
|
124
164
|
const to = readStringParam(params, "to", { required: true });
|
|
125
165
|
const question = readStringParam(params, "pollQuestion", {
|
|
@@ -78,14 +78,14 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
|
|
|
78
78
|
Array.isArray(rawComponents) || typeof rawComponents === "function"
|
|
79
79
|
? (rawComponents as DiscordSendComponents)
|
|
80
80
|
: undefined;
|
|
81
|
-
const content = readStringParam(ctx.params, "content", {
|
|
82
|
-
required: !asVoice && !componentSpec && !components,
|
|
83
|
-
allowEmpty: true,
|
|
84
|
-
});
|
|
85
81
|
const mediaUrl =
|
|
86
82
|
readStringParam(ctx.params, "mediaUrl", { trim: false }) ??
|
|
87
83
|
readStringParam(ctx.params, "path", { trim: false }) ??
|
|
88
84
|
readStringParam(ctx.params, "filePath", { trim: false });
|
|
85
|
+
const content = readStringParam(ctx.params, "content", {
|
|
86
|
+
required: !asVoice && !componentSpec && !components && !mediaUrl,
|
|
87
|
+
allowEmpty: true,
|
|
88
|
+
});
|
|
89
89
|
const filename = readStringParam(ctx.params, "filename");
|
|
90
90
|
const replyTo = readStringParam(ctx.params, "replyTo");
|
|
91
91
|
const rawEmbeds = ctx.params.embeds;
|
|
@@ -117,6 +117,9 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
|
|
|
117
117
|
agentId: agentId ?? undefined,
|
|
118
118
|
mediaUrl: mediaUrl ?? undefined,
|
|
119
119
|
filename: filename ?? undefined,
|
|
120
|
+
mediaAccess: ctx.options?.mediaAccess,
|
|
121
|
+
mediaLocalRoots: ctx.options?.mediaLocalRoots,
|
|
122
|
+
mediaReadFile: ctx.options?.mediaReadFile,
|
|
120
123
|
},
|
|
121
124
|
);
|
|
122
125
|
return jsonResult({ ok: true, result, components: true });
|
|
@@ -144,6 +147,7 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
|
|
|
144
147
|
|
|
145
148
|
const result = await discordMessagingActionRuntime.sendMessageDiscord(to, content ?? "", {
|
|
146
149
|
...ctx.withOpts(),
|
|
150
|
+
mediaAccess: ctx.options?.mediaAccess,
|
|
147
151
|
mediaUrl,
|
|
148
152
|
filename: filename ?? undefined,
|
|
149
153
|
mediaLocalRoots: ctx.options?.mediaLocalRoots,
|
|
@@ -12,6 +12,11 @@ import { discordMessagingActionRuntime } from "./runtime.messaging.runtime.js";
|
|
|
12
12
|
import { createDiscordActionOptions } from "./runtime.shared.js";
|
|
13
13
|
|
|
14
14
|
export type DiscordMessagingActionOptions = {
|
|
15
|
+
mediaAccess?: {
|
|
16
|
+
localRoots?: readonly string[];
|
|
17
|
+
readFile?: (filePath: string) => Promise<Buffer>;
|
|
18
|
+
workspaceDir?: string;
|
|
19
|
+
};
|
|
15
20
|
mediaLocalRoots?: readonly string[];
|
|
16
21
|
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
17
22
|
};
|
|
@@ -93,6 +93,11 @@ function handleMessagingAction(
|
|
|
93
93
|
isActionEnabled: (key: keyof DiscordActionConfig) => boolean,
|
|
94
94
|
cfg: OpenClawConfig = DISCORD_TEST_CFG,
|
|
95
95
|
options?: {
|
|
96
|
+
mediaAccess?: {
|
|
97
|
+
localRoots?: readonly string[];
|
|
98
|
+
readFile?: (filePath: string) => Promise<Buffer>;
|
|
99
|
+
workspaceDir?: string;
|
|
100
|
+
};
|
|
96
101
|
mediaLocalRoots?: readonly string[];
|
|
97
102
|
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
98
103
|
},
|
|
@@ -463,6 +468,8 @@ describe("handleDiscordMessagingAction", () => {
|
|
|
463
468
|
|
|
464
469
|
it("forwards trusted mediaLocalRoots into sendMessageDiscord", async () => {
|
|
465
470
|
sendMessageDiscord.mockClear();
|
|
471
|
+
const mediaReadFile = vi.fn(async () => Buffer.from("image"));
|
|
472
|
+
const mediaAccess = { localRoots: ["/tmp/agent-root"], readFile: mediaReadFile };
|
|
466
473
|
await handleMessagingAction(
|
|
467
474
|
"sendMessage",
|
|
468
475
|
{
|
|
@@ -472,11 +479,35 @@ describe("handleDiscordMessagingAction", () => {
|
|
|
472
479
|
},
|
|
473
480
|
enableAllActions,
|
|
474
481
|
DISCORD_TEST_CFG,
|
|
475
|
-
{ mediaLocalRoots: ["/tmp/agent-root"] },
|
|
482
|
+
{ mediaAccess, mediaLocalRoots: ["/tmp/agent-root"], mediaReadFile },
|
|
476
483
|
);
|
|
477
484
|
expect(sendMessageDiscord).toHaveBeenCalledWith(
|
|
478
485
|
"channel:123",
|
|
479
486
|
"hello",
|
|
487
|
+
expect.objectContaining({
|
|
488
|
+
mediaAccess,
|
|
489
|
+
mediaUrl: "/tmp/image.png",
|
|
490
|
+
mediaLocalRoots: ["/tmp/agent-root"],
|
|
491
|
+
mediaReadFile,
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("allows media-only message sends", async () => {
|
|
497
|
+
sendMessageDiscord.mockClear();
|
|
498
|
+
await handleMessagingAction(
|
|
499
|
+
"sendMessage",
|
|
500
|
+
{
|
|
501
|
+
to: "channel:123",
|
|
502
|
+
mediaUrl: "/tmp/image.png",
|
|
503
|
+
},
|
|
504
|
+
enableAllActions,
|
|
505
|
+
DISCORD_TEST_CFG,
|
|
506
|
+
{ mediaLocalRoots: ["/tmp/agent-root"] },
|
|
507
|
+
);
|
|
508
|
+
expect(sendMessageDiscord).toHaveBeenCalledWith(
|
|
509
|
+
"channel:123",
|
|
510
|
+
"",
|
|
480
511
|
expect.objectContaining({
|
|
481
512
|
mediaUrl: "/tmp/image.png",
|
|
482
513
|
mediaLocalRoots: ["/tmp/agent-root"],
|
package/src/actions/runtime.ts
CHANGED
|
@@ -58,7 +58,13 @@ export async function handleDiscordAction(
|
|
|
58
58
|
params: Record<string, unknown>,
|
|
59
59
|
cfg: OpenClawConfig,
|
|
60
60
|
options?: {
|
|
61
|
+
mediaAccess?: {
|
|
62
|
+
localRoots?: readonly string[];
|
|
63
|
+
readFile?: (filePath: string) => Promise<Buffer>;
|
|
64
|
+
workspaceDir?: string;
|
|
65
|
+
};
|
|
61
66
|
mediaLocalRoots?: readonly string[];
|
|
67
|
+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
62
68
|
},
|
|
63
69
|
): Promise<AgentToolResult<unknown>> {
|
|
64
70
|
const action = readStringParam(params, "action", { required: true });
|
|
@@ -55,7 +55,15 @@ describe("discordMessageActions", () => {
|
|
|
55
55
|
expect(discovery?.capabilities).toEqual(["presentation"]);
|
|
56
56
|
expect(discovery?.schema).toBeUndefined();
|
|
57
57
|
expect(discovery?.actions).toEqual(
|
|
58
|
-
expect.arrayContaining([
|
|
58
|
+
expect.arrayContaining([
|
|
59
|
+
"send",
|
|
60
|
+
"upload-file",
|
|
61
|
+
"poll",
|
|
62
|
+
"react",
|
|
63
|
+
"reactions",
|
|
64
|
+
"emoji-list",
|
|
65
|
+
"permissions",
|
|
66
|
+
]),
|
|
59
67
|
);
|
|
60
68
|
expect(discovery?.actions).not.toContain("channel-create");
|
|
61
69
|
expect(discovery?.actions).not.toContain("role-add");
|
|
@@ -144,13 +152,35 @@ describe("discordMessageActions", () => {
|
|
|
144
152
|
});
|
|
145
153
|
|
|
146
154
|
expect(defaultDiscovery?.actions).toEqual(expect.arrayContaining(["send", "poll"]));
|
|
155
|
+
expect(defaultDiscovery?.actions).toContain("upload-file");
|
|
147
156
|
expect(defaultDiscovery?.actions).not.toContain("react");
|
|
148
157
|
expect(workDiscovery?.actions).toEqual(
|
|
149
|
-
expect.arrayContaining(["send", "react", "reactions", "emoji-list"]),
|
|
158
|
+
expect.arrayContaining(["send", "upload-file", "react", "reactions", "emoji-list"]),
|
|
150
159
|
);
|
|
151
160
|
expect(workDiscovery?.actions).not.toContain("poll");
|
|
152
161
|
});
|
|
153
162
|
|
|
163
|
+
it("hides upload-file when Discord message actions are disabled", () => {
|
|
164
|
+
const discovery = discordMessageActions.describeMessageTool?.({
|
|
165
|
+
cfg: {
|
|
166
|
+
channels: {
|
|
167
|
+
discord: {
|
|
168
|
+
token: "Bot token-main",
|
|
169
|
+
actions: {
|
|
170
|
+
messages: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
} as OpenClawConfig,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(discovery?.actions).toContain("send");
|
|
178
|
+
expect(discovery?.actions).not.toContain("upload-file");
|
|
179
|
+
expect(discovery?.actions).not.toContain("read");
|
|
180
|
+
expect(discovery?.actions).not.toContain("edit");
|
|
181
|
+
expect(discovery?.actions).not.toContain("delete");
|
|
182
|
+
});
|
|
183
|
+
|
|
154
184
|
it("does not expose Discord-native message tool schema", () => {
|
|
155
185
|
const discovery = discordMessageActions.describeMessageTool?.({
|
|
156
186
|
cfg: {
|
|
@@ -170,7 +200,7 @@ describe("discordMessageActions", () => {
|
|
|
170
200
|
);
|
|
171
201
|
});
|
|
172
202
|
|
|
173
|
-
it.each(["send", "edit", "delete", "react", "pin", "poll"])(
|
|
203
|
+
it.each(["send", "upload-file", "edit", "delete", "react", "pin", "poll"])(
|
|
174
204
|
"routes %s actions through local execution mode",
|
|
175
205
|
(action) => {
|
|
176
206
|
expect(discordMessageActions.resolveExecutionMode?.({ action: action as never })).toBe(
|
|
@@ -210,6 +240,11 @@ describe("discordMessageActions", () => {
|
|
|
210
240
|
const toolContext: ChannelMessageActionContext["toolContext"] = {
|
|
211
241
|
currentChannelProvider: "discord",
|
|
212
242
|
};
|
|
243
|
+
const mediaReadFile = vi.fn(async () => Buffer.from("image"));
|
|
244
|
+
const mediaAccess: NonNullable<ChannelMessageActionContext["mediaAccess"]> = {
|
|
245
|
+
localRoots: ["/tmp/media"],
|
|
246
|
+
readFile: mediaReadFile,
|
|
247
|
+
};
|
|
213
248
|
const mediaLocalRoots = ["/tmp/media"];
|
|
214
249
|
|
|
215
250
|
await discordMessageActions.handleAction?.({
|
|
@@ -220,7 +255,9 @@ describe("discordMessageActions", () => {
|
|
|
220
255
|
accountId: "ops",
|
|
221
256
|
requesterSenderId: "user-1",
|
|
222
257
|
toolContext,
|
|
258
|
+
mediaAccess,
|
|
223
259
|
mediaLocalRoots,
|
|
260
|
+
mediaReadFile,
|
|
224
261
|
});
|
|
225
262
|
|
|
226
263
|
expect(handleDiscordMessageActionMock).toHaveBeenCalledWith({
|
|
@@ -230,7 +267,9 @@ describe("discordMessageActions", () => {
|
|
|
230
267
|
accountId: "ops",
|
|
231
268
|
requesterSenderId: "user-1",
|
|
232
269
|
toolContext,
|
|
270
|
+
mediaAccess,
|
|
233
271
|
mediaLocalRoots,
|
|
272
|
+
mediaReadFile,
|
|
234
273
|
});
|
|
235
274
|
});
|
|
236
275
|
});
|
package/src/channel-actions.ts
CHANGED
|
@@ -86,6 +86,7 @@ function describeDiscordMessageTool({
|
|
|
86
86
|
actions.add("emoji-list");
|
|
87
87
|
}
|
|
88
88
|
if (discovery.isEnabled("messages")) {
|
|
89
|
+
actions.add("upload-file");
|
|
89
90
|
actions.add("read");
|
|
90
91
|
actions.add("edit");
|
|
91
92
|
actions.add("delete");
|
|
@@ -181,7 +182,9 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
|
|
181
182
|
accountId,
|
|
182
183
|
requesterSenderId,
|
|
183
184
|
toolContext,
|
|
185
|
+
mediaAccess,
|
|
184
186
|
mediaLocalRoots,
|
|
187
|
+
mediaReadFile,
|
|
185
188
|
}) => {
|
|
186
189
|
return await (
|
|
187
190
|
await loadDiscordChannelActionsRuntime()
|
|
@@ -192,7 +195,9 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
|
|
192
195
|
accountId,
|
|
193
196
|
requesterSenderId,
|
|
194
197
|
toolContext,
|
|
198
|
+
mediaAccess,
|
|
195
199
|
mediaLocalRoots,
|
|
200
|
+
mediaReadFile,
|
|
196
201
|
});
|
|
197
202
|
},
|
|
198
203
|
};
|
package/src/channel-api.ts
CHANGED
|
@@ -18,6 +18,7 @@ const DISCORD_CHANNEL_META = {
|
|
|
18
18
|
blurb: "very well supported right now.",
|
|
19
19
|
systemImage: "bubble.left.and.bubble.right",
|
|
20
20
|
markdownCapable: true,
|
|
21
|
+
preferSessionLookupForAnnounceTarget: true,
|
|
21
22
|
} as const;
|
|
22
23
|
|
|
23
24
|
export function getChatChannelMeta(id: string) {
|
package/src/channel.test.ts
CHANGED
|
@@ -119,6 +119,14 @@ describe("discordPlugin outbound", () => {
|
|
|
119
119
|
expect(discordPlugin.outbound?.preferFinalAssistantVisibleText).toBe(true);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it("adds Discord mention formatting to agent prompt hints", () => {
|
|
123
|
+
const hints = discordPlugin.agentPrompt?.messageToolHints?.({} as never) ?? [];
|
|
124
|
+
|
|
125
|
+
expect(hints).toContain(
|
|
126
|
+
"- Discord mentions: use canonical outbound syntax: users `<@USER_ID>`, channels `<#CHANNEL_ID>`, and roles `<@&ROLE_ID>`. Do not use the legacy `<@!USER_ID>` nickname form.",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
122
130
|
it("preserves normalized explicit Discord targets for delivery routing", () => {
|
|
123
131
|
const parseExplicitTarget = discordPlugin.messaging?.parseExplicitTarget;
|
|
124
132
|
if (!parseExplicitTarget) {
|
package/src/channel.ts
CHANGED
|
@@ -213,6 +213,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
|
|
213
213
|
},
|
|
214
214
|
agentPrompt: {
|
|
215
215
|
messageToolHints: () => [
|
|
216
|
+
"- Discord mentions: use canonical outbound syntax: users `<@USER_ID>`, channels `<#CHANNEL_ID>`, and roles `<@&ROLE_ID>`. Do not use the legacy `<@!USER_ID>` nickname form.",
|
|
216
217
|
"- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.",
|
|
217
218
|
"- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages.",
|
|
218
219
|
],
|
package/src/client.ts
CHANGED
|
@@ -61,13 +61,6 @@ function resolveToken(params: { accountId: string; fallbackToken?: string }) {
|
|
|
61
61
|
return fallback;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function resolveDiscordProxyFetch(
|
|
65
|
-
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
|
|
66
|
-
runtime?: Pick<RuntimeEnv, "error">,
|
|
67
|
-
): typeof fetch | undefined {
|
|
68
|
-
return resolveDiscordClientAccountContext(opts, runtime).proxyFetch;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
64
|
function resolveRest(
|
|
72
65
|
token: string,
|
|
73
66
|
account: ResolvedDiscordAccount,
|
package/src/config-ui-hints.ts
CHANGED
|
@@ -113,13 +113,13 @@ export const discordChannelConfigUiHints = {
|
|
|
113
113
|
label: "Discord Thread Binding Max Age (hours)",
|
|
114
114
|
help: "Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
|
115
115
|
},
|
|
116
|
-
"threadBindings.
|
|
117
|
-
label: "Discord Thread-Bound
|
|
118
|
-
help: "Allow
|
|
116
|
+
"threadBindings.spawnSessions": {
|
|
117
|
+
label: "Discord Thread-Bound Session Spawn",
|
|
118
|
+
help: "Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel.",
|
|
119
119
|
},
|
|
120
|
-
"threadBindings.
|
|
121
|
-
label: "Discord Thread
|
|
122
|
-
help:
|
|
120
|
+
"threadBindings.defaultSpawnContext": {
|
|
121
|
+
label: "Discord Thread Spawn Context",
|
|
122
|
+
help: 'Default native subagent context for thread-bound spawns. "fork" starts from the requester transcript; "isolated" starts clean. Default: "fork".',
|
|
123
123
|
},
|
|
124
124
|
"ui.components.accentColor": {
|
|
125
125
|
label: "Discord Component Accent Color",
|