@aspalagin/openclaw-max 0.7.3 → 0.8.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/LICENSE +2 -0
- package/README.md +412 -344
- package/README_EN.md +492 -0
- package/SKILL.md +45 -37
- package/assets/icon.png +0 -0
- package/dist/secret-contract-api.js +6 -0
- package/dist/src/access-policy.js +123 -0
- package/dist/src/accounts.js +170 -23
- package/dist/src/action-scope.js +213 -0
- package/dist/src/actions.js +159 -31
- package/dist/src/api.js +99 -83
- package/dist/src/callbacks.js +27 -7
- package/dist/src/channel-agent-prompt.js +8 -19
- package/dist/src/channel-config.js +25 -3
- package/dist/src/channel-lifecycle.js +28 -5
- package/dist/src/channel-outbound.js +43 -9
- package/dist/src/channel-policy.js +19 -6
- package/dist/src/channel.js +16 -0
- package/dist/src/command-menu.js +199 -0
- package/dist/src/config-schema.js +106 -10
- package/dist/src/deliver.js +144 -48
- package/dist/src/dispatch.js +12 -11
- package/dist/src/gateway-drain.js +40 -0
- package/dist/src/group-mention.js +129 -0
- package/dist/src/inbound-attachments.js +162 -20
- package/dist/src/inbound.js +388 -134
- package/dist/src/media-temp.js +31 -20
- package/dist/src/network.js +216 -0
- package/dist/src/onboarding.js +4 -2
- package/dist/src/polling.js +180 -12
- package/dist/src/presentation.js +11 -0
- package/dist/src/progress-draft.js +168 -0
- package/dist/src/runtime.js +15 -0
- package/dist/src/secret-contract.js +149 -0
- package/dist/src/send.js +250 -46
- package/dist/src/stream-draft.js +83 -41
- package/dist/src/turn-adoption.js +24 -0
- package/dist/src/typing.js +55 -0
- package/dist/src/update-journal.js +267 -0
- package/dist/src/webhook-queue.js +151 -26
- package/dist/src/webhook-runner.js +61 -9
- package/dist/src/webhook.js +34 -15
- package/openclaw.plugin.json +983 -8
- package/package.json +17 -4
- package/src/access-policy.ts +154 -0
- package/src/accounts.ts +260 -25
- package/src/action-scope.ts +274 -0
- package/src/actions.ts +188 -35
- package/src/api.ts +158 -101
- package/src/callbacks.ts +26 -7
- package/src/channel-agent-prompt.ts +8 -19
- package/src/channel-config.ts +32 -3
- package/src/channel-lifecycle.ts +33 -6
- package/src/channel-outbound.ts +50 -7
- package/src/channel-policy.ts +27 -7
- package/src/channel.ts +16 -0
- package/src/command-menu.ts +277 -0
- package/src/config-schema.ts +111 -10
- package/src/deliver.ts +174 -55
- package/src/dispatch.ts +12 -11
- package/src/gateway-drain.ts +45 -0
- package/src/group-mention.ts +191 -0
- package/src/inbound-attachments.ts +173 -22
- package/src/inbound.ts +443 -130
- package/src/media-temp.ts +35 -23
- package/src/network.ts +233 -0
- package/src/onboarding.ts +4 -2
- package/src/polling.ts +221 -15
- package/src/presentation.ts +14 -1
- package/src/progress-draft.ts +226 -0
- package/src/runtime.ts +23 -0
- package/src/secret-contract.ts +183 -0
- package/src/send.ts +346 -59
- package/src/stream-draft.ts +102 -41
- package/src/turn-adoption.ts +33 -0
- package/src/types.ts +9 -3
- package/src/typing.ts +77 -0
- package/src/update-journal.ts +302 -0
- package/src/webhook-queue.ts +205 -33
- package/src/webhook-runner.ts +65 -8
- package/src/webhook.ts +39 -18
- package/scripts/check-cycles.mjs +0 -38
- package/scripts/test-api.mjs +0 -169
- package/scripts/test-send.mjs +0 -162
- package/scripts/update-schema.mjs +0 -48
package/src/actions.ts
CHANGED
|
@@ -2,22 +2,37 @@
|
|
|
2
2
|
* MAX channel message actions adapter — implements message tool actions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { createActionGate } from 'openclaw/plugin-sdk/channel-actions';
|
|
6
|
+
import type {
|
|
7
|
+
ChannelMessageActionAdapter,
|
|
8
|
+
ChannelMessageActionContext,
|
|
9
|
+
} from 'openclaw/plugin-sdk/channel-contract';
|
|
6
10
|
import type { OpenClawConfig } from 'openclaw/plugin-sdk/core';
|
|
7
11
|
import { normalizeMessagePresentation } from 'openclaw/plugin-sdk/interactive-runtime';
|
|
12
|
+
import { getAgentScopedMediaLocalRoots } from 'openclaw/plugin-sdk/media-local-roots';
|
|
8
13
|
import { readStringParam } from 'openclaw/plugin-sdk/param-readers';
|
|
9
14
|
import { jsonResult } from 'openclaw/plugin-sdk/tool-results';
|
|
10
15
|
|
|
11
16
|
import { listMaxAccountIds, resolveMaxAccount } from './accounts.js';
|
|
12
|
-
import {
|
|
17
|
+
import { assertMaxActionInScope } from './action-scope.js';
|
|
18
|
+
import { sanitizeMaxFileName } from './media-temp.js';
|
|
19
|
+
import {
|
|
20
|
+
materializeMaxPresentation,
|
|
21
|
+
readMaxDeliveryPin,
|
|
22
|
+
resolveMaxTextChunkLimit,
|
|
23
|
+
} from './presentation.js';
|
|
13
24
|
import { getMaxRuntime } from './runtime.js';
|
|
14
25
|
import {
|
|
15
26
|
deleteMaxMessage,
|
|
27
|
+
describeMaxMediaSource,
|
|
16
28
|
editMaxMessage,
|
|
29
|
+
type MaxLocalMediaAccess,
|
|
17
30
|
type MaxMediaSendOptions,
|
|
31
|
+
type MaxMediaSource,
|
|
18
32
|
pinMaxMessage,
|
|
19
33
|
readMaxChannelButtons,
|
|
20
34
|
readMaxSendButtons,
|
|
35
|
+
resolveMaxSendFlags,
|
|
21
36
|
sendMaxContact,
|
|
22
37
|
sendMaxLocation,
|
|
23
38
|
sendMaxMediaGroup,
|
|
@@ -30,6 +45,72 @@ import { getLastStickerCode } from './sticker-cache.js';
|
|
|
30
45
|
|
|
31
46
|
const providerId = 'max';
|
|
32
47
|
const mediaSourceKeys = ['media', 'filePath', 'path', 'fileUrl', 'url', 'buffer', 'image'] as const;
|
|
48
|
+
/**
|
|
49
|
+
* Params that carry a media path or URL, per action: core normalizes them for
|
|
50
|
+
* the sandbox and scopes outbound media access to them. `buffer` is base64
|
|
51
|
+
* content for core, not a path, so it is not declared.
|
|
52
|
+
*/
|
|
53
|
+
const mediaSourceParams = mediaSourceKeys.filter((key) => key !== 'buffer');
|
|
54
|
+
|
|
55
|
+
/** `data:<type>;base64,<data>` — a data URL in `buffer`. */
|
|
56
|
+
const DATA_URL_RE = /^data:([^;,]*)(?:;[^,]*)?;base64,/i;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Inline content from `buffer` (base64 or a base64 data URL) with `filename`
|
|
60
|
+
* and `contentType` (aliases fileName, mimeType) of the same object. The size
|
|
61
|
+
* limit is checked before decoding, so an oversized payload is refused
|
|
62
|
+
* without allocating it.
|
|
63
|
+
*/
|
|
64
|
+
function readInlineMedia(
|
|
65
|
+
source: Record<string, unknown>,
|
|
66
|
+
maxBytes: number,
|
|
67
|
+
): MaxMediaSource | undefined {
|
|
68
|
+
const raw = typeof source.buffer === 'string' ? source.buffer : undefined;
|
|
69
|
+
if (!raw?.trim()) return undefined;
|
|
70
|
+
const dataUrl = DATA_URL_RE.exec(raw);
|
|
71
|
+
const base64 = (dataUrl ? raw.slice(dataUrl[0].length) : raw).replace(/\s+/g, '');
|
|
72
|
+
if (!/^[A-Za-z0-9+/_-]*={0,2}$/.test(base64)) {
|
|
73
|
+
throw new Error('buffer is not valid base64 content');
|
|
74
|
+
}
|
|
75
|
+
const decodedBytes = Math.floor((base64.replace(/=+$/, '').length * 3) / 4);
|
|
76
|
+
if (decodedBytes > maxBytes) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`buffer is ${decodedBytes} bytes, over the ${maxBytes}-byte media limit (mediaMaxMb)`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const buffer = Buffer.from(
|
|
82
|
+
base64,
|
|
83
|
+
base64.includes('-') || base64.includes('_') ? 'base64url' : 'base64',
|
|
84
|
+
);
|
|
85
|
+
if (buffer.byteLength === 0) throw new Error('buffer is empty');
|
|
86
|
+
const pick = (...keys: string[]) =>
|
|
87
|
+
keys
|
|
88
|
+
.map((key) => source[key])
|
|
89
|
+
.find((value): value is string => typeof value === 'string' && value.trim() !== '')
|
|
90
|
+
?.trim();
|
|
91
|
+
const contentType = pick('contentType', 'mimeType') ?? (dataUrl?.[1] || undefined);
|
|
92
|
+
return {
|
|
93
|
+
buffer,
|
|
94
|
+
fileName: sanitizeMaxFileName(pick('filename', 'fileName'), contentType),
|
|
95
|
+
contentType,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Local media access for an action: the roots and host reader core passed
|
|
101
|
+
* with the action; without them (direct plugin dispatch) the agent-scoped
|
|
102
|
+
* default roots. Local files are read only through this policy.
|
|
103
|
+
*/
|
|
104
|
+
function resolveActionLocalMedia(ctx: ChannelMessageActionContext): MaxLocalMediaAccess {
|
|
105
|
+
if (ctx.mediaAccess || ctx.mediaLocalRoots?.length || ctx.mediaReadFile) {
|
|
106
|
+
return {
|
|
107
|
+
mediaAccess: ctx.mediaAccess,
|
|
108
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
109
|
+
mediaReadFile: ctx.mediaReadFile,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { mediaLocalRoots: getAgentScopedMediaLocalRoots(ctx.cfg, ctx.agentId ?? undefined) };
|
|
113
|
+
}
|
|
33
114
|
|
|
34
115
|
function listEnabledAccounts(cfg: OpenClawConfig) {
|
|
35
116
|
return listMaxAccountIds(cfg)
|
|
@@ -49,32 +130,29 @@ function readTargetParam(params: Record<string, unknown>, required = true): stri
|
|
|
49
130
|
/**
|
|
50
131
|
* Media sources in order: the direct media field first, then every
|
|
51
132
|
* structured `attachments[]` item (one source per item). Duplicates dropped.
|
|
133
|
+
* A path or URL wins over `buffer` (core may fill both; the path goes through
|
|
134
|
+
* the guarded loader); `buffer` alone is sent as inline content.
|
|
52
135
|
*/
|
|
53
|
-
function readMediaSources(params: Record<string, unknown
|
|
54
|
-
const sources:
|
|
55
|
-
const add = (value:
|
|
136
|
+
function readMediaSources(params: Record<string, unknown>, maxBytes: number): MaxMediaSource[] {
|
|
137
|
+
const sources: MaxMediaSource[] = [];
|
|
138
|
+
const add = (value: MaxMediaSource | undefined) => {
|
|
56
139
|
if (value && !sources.includes(value)) sources.push(value);
|
|
57
140
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
add(value);
|
|
63
|
-
break;
|
|
141
|
+
const readPath = (source: Record<string, unknown>): string | undefined => {
|
|
142
|
+
for (const key of mediaSourceParams) {
|
|
143
|
+
const value = readStringParam(source, key, { trim: false });
|
|
144
|
+
if (value) return value;
|
|
64
145
|
}
|
|
65
|
-
|
|
146
|
+
return undefined;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
add(readPath(params) ?? readInlineMedia(params, maxBytes));
|
|
66
150
|
|
|
67
151
|
if (Array.isArray(params.attachments)) {
|
|
68
152
|
for (const item of params.attachments) {
|
|
69
153
|
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
70
154
|
const attachment = item as Record<string, unknown>;
|
|
71
|
-
|
|
72
|
-
const value = typeof attachment[key] === 'string' ? attachment[key] : undefined;
|
|
73
|
-
if (value) {
|
|
74
|
-
add(value);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
155
|
+
add(readPath(attachment) ?? readInlineMedia(attachment, maxBytes));
|
|
78
156
|
}
|
|
79
157
|
}
|
|
80
158
|
|
|
@@ -89,7 +167,7 @@ function readMediaSources(params: Record<string, unknown>): string[] {
|
|
|
89
167
|
async function sendMaxMediaSources(
|
|
90
168
|
to: string,
|
|
91
169
|
caption: string,
|
|
92
|
-
sources:
|
|
170
|
+
sources: MaxMediaSource[],
|
|
93
171
|
opts: MaxMediaSendOptions,
|
|
94
172
|
): Promise<{
|
|
95
173
|
messageId: string;
|
|
@@ -113,7 +191,7 @@ async function sendMaxMediaSources(
|
|
|
113
191
|
...opts,
|
|
114
192
|
onError: (err, failed) => {
|
|
115
193
|
firstError ??= err;
|
|
116
|
-
errors.push(`${failed.join(', ')}: ${String(err)}`);
|
|
194
|
+
errors.push(`${failed.map(describeMaxMediaSource).join(', ')}: ${String(err)}`);
|
|
117
195
|
},
|
|
118
196
|
});
|
|
119
197
|
if (messageIds.length === 0 && firstError !== undefined) throw firstError;
|
|
@@ -160,6 +238,13 @@ function readLocationParams(
|
|
|
160
238
|
return { latitude: lat, longitude: lng };
|
|
161
239
|
}
|
|
162
240
|
|
|
241
|
+
/** Voice delivery of audio: core's `asVoice` (or its `audioAsVoice` alias), boolean or "true". */
|
|
242
|
+
function readAsVoiceParam(params: Record<string, unknown>): boolean {
|
|
243
|
+
return [params.asVoice, params.audioAsVoice].some(
|
|
244
|
+
(value) => value === true || (typeof value === 'string' && value.trim() === 'true'),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
163
248
|
/** Contact card fields: contactId (MAX user id) and vcfPhone/phone next to the given name. */
|
|
164
249
|
function readContactParams(
|
|
165
250
|
params: Record<string, unknown>,
|
|
@@ -174,20 +259,47 @@ function readContactParams(
|
|
|
174
259
|
};
|
|
175
260
|
}
|
|
176
261
|
|
|
262
|
+
const MAX_MESSAGE_ACTIONS = [
|
|
263
|
+
'send',
|
|
264
|
+
'edit',
|
|
265
|
+
'delete',
|
|
266
|
+
'sticker',
|
|
267
|
+
'sendAttachment',
|
|
268
|
+
'pin',
|
|
269
|
+
'unpin',
|
|
270
|
+
] as const;
|
|
271
|
+
|
|
177
272
|
export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
178
|
-
describeMessageTool: ({ cfg }) => {
|
|
273
|
+
describeMessageTool: ({ cfg, accountId }) => {
|
|
179
274
|
const accounts = listEnabledAccounts(cfg);
|
|
180
275
|
if (accounts.length === 0) {
|
|
181
276
|
return null;
|
|
182
277
|
}
|
|
278
|
+
// channels.max.actions.<action>: false hides the action from the tool.
|
|
279
|
+
const gate = createActionGate(resolveMaxAccount({ cfg, accountId }).config.actions);
|
|
183
280
|
return {
|
|
184
|
-
actions:
|
|
281
|
+
actions: MAX_MESSAGE_ACTIONS.filter((name) => gate(name)),
|
|
185
282
|
// presentation → inline keyboard + MAX markdown (presentation.ts);
|
|
186
283
|
// delivery-pin → PUT /chats/{chatId}/pin on the sent message.
|
|
187
284
|
capabilities: ['presentation', 'delivery-pin'],
|
|
285
|
+
mediaSourceParams: { send: mediaSourceParams, sendAttachment: mediaSourceParams },
|
|
188
286
|
};
|
|
189
287
|
},
|
|
190
288
|
|
|
289
|
+
// A send with a presentation is delivered by core itself (outbound adapter
|
|
290
|
+
// sendPayload), never reaching handleAction: the action gate and scope are
|
|
291
|
+
// checked here first, then core delivers the payload unchanged. Any other
|
|
292
|
+
// send declines (null) and keeps the handleAction path, which checks them.
|
|
293
|
+
prepareSendPayload: async ({ ctx, to, payload, replyToId }) => {
|
|
294
|
+
if (!payload.presentation) return null;
|
|
295
|
+
const account = resolveMaxAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
296
|
+
if (!createActionGate(account.config.actions)('send')) {
|
|
297
|
+
throw new Error('MAX action "send" is disabled by channels.max.actions');
|
|
298
|
+
}
|
|
299
|
+
await assertMaxActionInScope(ctx, account, { to, replyTo: replyToId });
|
|
300
|
+
return payload;
|
|
301
|
+
},
|
|
302
|
+
|
|
191
303
|
extractToolSend: ({ args }: { args: Record<string, unknown> }) => {
|
|
192
304
|
// Extract routing info for ALL actions (send, edit, delete, sticker)
|
|
193
305
|
// Core uses extractToolSend for routing all message tool actions to plugin
|
|
@@ -209,13 +321,14 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
209
321
|
if (!to) {
|
|
210
322
|
return null;
|
|
211
323
|
}
|
|
212
|
-
// Strip provider prefix (e.g. "max:
|
|
324
|
+
// Strip provider prefix (e.g. "max:123456789" → "123456789")
|
|
213
325
|
if (to.startsWith('max:')) to = to.slice(4);
|
|
214
326
|
const accountId = typeof args.accountId === 'string' ? args.accountId.trim() : undefined;
|
|
215
327
|
return { to, accountId };
|
|
216
328
|
},
|
|
217
329
|
|
|
218
|
-
handleAction: async (
|
|
330
|
+
handleAction: async (ctx) => {
|
|
331
|
+
const { action, params, cfg, accountId } = ctx;
|
|
219
332
|
const account = resolveMaxAccount({
|
|
220
333
|
cfg,
|
|
221
334
|
accountId,
|
|
@@ -223,8 +336,17 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
223
336
|
if (!account.token) {
|
|
224
337
|
throw new Error('MAX bot token not configured');
|
|
225
338
|
}
|
|
339
|
+
if (!createActionGate(account.config.actions)(action)) {
|
|
340
|
+
throw new Error(`MAX action "${action}" is disabled by channels.max.actions`);
|
|
341
|
+
}
|
|
226
342
|
|
|
227
|
-
//
|
|
343
|
+
// Notification and link-preview options of sends: core's `silent` flag
|
|
344
|
+
// beats the account defaults (channels.max.notify / disableLinkPreview).
|
|
345
|
+
const sendFlags = resolveMaxSendFlags(account.config, {
|
|
346
|
+
silent: typeof params.silent === 'boolean' ? params.silent : undefined,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Strip provider prefix from target (e.g. "max:123456789" → "123456789")
|
|
228
350
|
const stripPrefix = (val: string | undefined): string | undefined => {
|
|
229
351
|
if (!val) return val;
|
|
230
352
|
return val.startsWith('max:') ? val.slice(4) : val;
|
|
@@ -232,6 +354,10 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
232
354
|
|
|
233
355
|
if (action === 'send') {
|
|
234
356
|
const to = stripPrefix(readTargetParam(params))!;
|
|
357
|
+
await assertMaxActionInScope(ctx, account, {
|
|
358
|
+
to,
|
|
359
|
+
replyTo: readStringParam(params, 'replyTo'),
|
|
360
|
+
});
|
|
235
361
|
const presentation = normalizeMessagePresentation(params.presentation);
|
|
236
362
|
const pin = readMaxDeliveryPin(params.delivery, params.pin);
|
|
237
363
|
const content =
|
|
@@ -285,15 +411,17 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
285
411
|
const rendered = await materializeMaxPresentation({ text: content, presentation });
|
|
286
412
|
const renderedButtons = readMaxChannelButtons(rendered.channelData);
|
|
287
413
|
const text = rendered.text ?? '';
|
|
414
|
+
const chunkLimit = resolveMaxTextChunkLimit(cfg, account.accountId);
|
|
288
415
|
const chunks =
|
|
289
|
-
text.length >
|
|
290
|
-
? getMaxRuntime().channel.text.chunkMarkdownText(text,
|
|
416
|
+
text.length > chunkLimit
|
|
417
|
+
? getMaxRuntime().channel.text.chunkMarkdownText(text, chunkLimit)
|
|
291
418
|
: [text];
|
|
292
419
|
let firstMessageId = '';
|
|
293
420
|
let firstChatType: string | undefined;
|
|
294
421
|
for (let index = 0; index < chunks.length; index += 1) {
|
|
295
422
|
const sent = await sendMaxMessage(to, chunks[index], {
|
|
296
423
|
token: account.token,
|
|
424
|
+
...sendFlags,
|
|
297
425
|
replyToMessageId:
|
|
298
426
|
index === 0 ? (readStringParam(params, 'replyTo') ?? undefined) : undefined,
|
|
299
427
|
format: 'markdown',
|
|
@@ -323,6 +451,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
323
451
|
if (firstCode) {
|
|
324
452
|
const result = await sendMaxSticker(to, firstCode, {
|
|
325
453
|
token: account.token,
|
|
454
|
+
...sendFlags,
|
|
326
455
|
replyToMessageId: replyTo ?? undefined,
|
|
327
456
|
});
|
|
328
457
|
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
@@ -334,6 +463,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
334
463
|
if (location) {
|
|
335
464
|
const result = await sendMaxLocation(to, location, content || undefined, {
|
|
336
465
|
token: account.token,
|
|
466
|
+
...sendFlags,
|
|
337
467
|
replyToMessageId: replyTo ?? undefined,
|
|
338
468
|
format: 'markdown',
|
|
339
469
|
});
|
|
@@ -345,28 +475,35 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
345
475
|
if (contactName) {
|
|
346
476
|
const result = await sendMaxContact(to, readContactParams(params, contactName), {
|
|
347
477
|
token: account.token,
|
|
478
|
+
...sendFlags,
|
|
348
479
|
replyToMessageId: replyTo ?? undefined,
|
|
349
480
|
});
|
|
350
481
|
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
351
482
|
}
|
|
352
483
|
|
|
353
484
|
// Media: direct media fields and every structured attachments[] item.
|
|
354
|
-
const
|
|
485
|
+
const mediaMaxBytes = (account.config.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
486
|
+
const mediaSources = readMediaSources(params, mediaMaxBytes);
|
|
355
487
|
|
|
356
488
|
if (mediaSources.length) {
|
|
357
|
-
// Local
|
|
358
|
-
//
|
|
489
|
+
// Local paths only under the allowed roots (resolveActionLocalMedia);
|
|
490
|
+
// public https image links go by URL, other remote media is
|
|
491
|
+
// downloaded through the SSRF-guarded fetcher and uploaded.
|
|
359
492
|
const result = await sendMaxMediaSources(to, content, mediaSources, {
|
|
360
493
|
token: account.token,
|
|
494
|
+
...sendFlags,
|
|
361
495
|
replyToMessageId: replyTo ?? undefined,
|
|
362
496
|
format: 'markdown',
|
|
363
|
-
mediaMaxBytes
|
|
497
|
+
mediaMaxBytes,
|
|
498
|
+
localMedia: resolveActionLocalMedia(ctx),
|
|
499
|
+
asVoice: readAsVoiceParam(params),
|
|
364
500
|
});
|
|
365
501
|
return withPin(result.messageId, result.chatType, mediaResultExtra(result));
|
|
366
502
|
}
|
|
367
503
|
|
|
368
504
|
const result = await sendMaxMessage(to, content, {
|
|
369
505
|
token: account.token,
|
|
506
|
+
...sendFlags,
|
|
370
507
|
replyToMessageId: replyTo ?? undefined,
|
|
371
508
|
format: 'markdown',
|
|
372
509
|
buttons,
|
|
@@ -376,6 +513,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
376
513
|
|
|
377
514
|
if (action === 'edit') {
|
|
378
515
|
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
516
|
+
await assertMaxActionInScope(ctx, account, { messageId });
|
|
379
517
|
const text = readStringParam(params, 'message', {
|
|
380
518
|
required: true,
|
|
381
519
|
allowEmpty: true,
|
|
@@ -389,6 +527,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
389
527
|
|
|
390
528
|
if (action === 'delete') {
|
|
391
529
|
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
530
|
+
await assertMaxActionInScope(ctx, account, { messageId });
|
|
392
531
|
await deleteMaxMessage(messageId, {
|
|
393
532
|
token: account.token,
|
|
394
533
|
});
|
|
@@ -398,6 +537,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
398
537
|
if (action === 'pin') {
|
|
399
538
|
const to = stripPrefix(readTargetParam(params))!;
|
|
400
539
|
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
540
|
+
await assertMaxActionInScope(ctx, account, { to });
|
|
401
541
|
const notifyParam = params.notify;
|
|
402
542
|
const pinned = await pinMaxMessage(to, messageId, {
|
|
403
543
|
token: account.token,
|
|
@@ -411,12 +551,17 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
411
551
|
|
|
412
552
|
if (action === 'unpin') {
|
|
413
553
|
const to = stripPrefix(readTargetParam(params))!;
|
|
554
|
+
await assertMaxActionInScope(ctx, account, { to });
|
|
414
555
|
await unpinMaxMessage(to, { token: account.token });
|
|
415
556
|
return jsonResult({ ok: true, to, pinned: false });
|
|
416
557
|
}
|
|
417
558
|
|
|
418
559
|
if (action === 'sticker') {
|
|
419
560
|
const to = stripPrefix(readTargetParam(params))!;
|
|
561
|
+
await assertMaxActionInScope(ctx, account, {
|
|
562
|
+
to,
|
|
563
|
+
replyTo: readStringParam(params, 'replyTo'),
|
|
564
|
+
});
|
|
420
565
|
// stickerId may come as string or string[] from message tool schema
|
|
421
566
|
const rawStickerId = params.stickerId;
|
|
422
567
|
let stickerCode: string | undefined = Array.isArray(rawStickerId)
|
|
@@ -433,6 +578,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
433
578
|
|
|
434
579
|
const result = await sendMaxSticker(to, stickerCode, {
|
|
435
580
|
token: account.token,
|
|
581
|
+
...sendFlags,
|
|
436
582
|
replyToMessageId: replyTo ?? undefined,
|
|
437
583
|
});
|
|
438
584
|
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
@@ -441,18 +587,23 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
441
587
|
if (action === 'sendAttachment') {
|
|
442
588
|
const to = stripPrefix(readTargetParam(params))!;
|
|
443
589
|
const replyTo = readStringParam(params, 'replyTo');
|
|
590
|
+
await assertMaxActionInScope(ctx, account, { to, replyTo });
|
|
444
591
|
const caption =
|
|
445
592
|
readStringParam(params, 'message') ?? readStringParam(params, 'caption') ?? '';
|
|
446
593
|
const attachType =
|
|
447
594
|
readStringParam(params, 'type') ?? readStringParam(params, 'attachmentType') ?? '';
|
|
448
|
-
const
|
|
595
|
+
const mediaMaxBytes = (account.config.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
596
|
+
const mediaSources = readMediaSources(params, mediaMaxBytes);
|
|
449
597
|
|
|
450
598
|
if (mediaSources.length) {
|
|
451
599
|
const result = await sendMaxMediaSources(to, caption, mediaSources, {
|
|
452
600
|
token: account.token,
|
|
601
|
+
...sendFlags,
|
|
453
602
|
replyToMessageId: replyTo ?? undefined,
|
|
454
603
|
format: 'markdown',
|
|
455
|
-
mediaMaxBytes
|
|
604
|
+
mediaMaxBytes,
|
|
605
|
+
localMedia: resolveActionLocalMedia(ctx),
|
|
606
|
+
asVoice: readAsVoiceParam(params),
|
|
456
607
|
});
|
|
457
608
|
return jsonResult({
|
|
458
609
|
ok: true,
|
|
@@ -473,6 +624,7 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
473
624
|
if (location) {
|
|
474
625
|
const result = await sendMaxLocation(to, location, caption || undefined, {
|
|
475
626
|
token: account.token,
|
|
627
|
+
...sendFlags,
|
|
476
628
|
replyToMessageId: replyTo ?? undefined,
|
|
477
629
|
});
|
|
478
630
|
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
@@ -486,13 +638,14 @@ export const maxMessageActions: ChannelMessageActionAdapter = {
|
|
|
486
638
|
readStringParam(params, 'contactName') ?? readStringParam(params, 'name') ?? 'Unknown';
|
|
487
639
|
const result = await sendMaxContact(to, readContactParams(params, contactName), {
|
|
488
640
|
token: account.token,
|
|
641
|
+
...sendFlags,
|
|
489
642
|
replyToMessageId: replyTo ?? undefined,
|
|
490
643
|
});
|
|
491
644
|
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
492
645
|
}
|
|
493
646
|
|
|
494
647
|
throw new Error(
|
|
495
|
-
"sendAttachment: unknown type. Use media/filePath for files, or type='location' / type='contact'",
|
|
648
|
+
"sendAttachment: unknown type. Use media/filePath (or base64 buffer with filename) for files, or type='location' / type='contact'",
|
|
496
649
|
);
|
|
497
650
|
}
|
|
498
651
|
|