@aspalagin/openclaw-max 0.7.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/LICENSE +21 -0
- package/README.md +424 -0
- package/SKILL.md +108 -0
- package/dist/index.js +17 -0
- package/dist/setup-entry.js +8 -0
- package/dist/src/accounts.js +148 -0
- package/dist/src/actions.js +421 -0
- package/dist/src/api.js +553 -0
- package/dist/src/callbacks.js +111 -0
- package/dist/src/channel-agent-prompt.js +38 -0
- package/dist/src/channel-config.js +103 -0
- package/dist/src/channel-directory.js +84 -0
- package/dist/src/channel-lifecycle.js +219 -0
- package/dist/src/channel-outbound.js +133 -0
- package/dist/src/channel-policy.js +130 -0
- package/dist/src/channel.js +76 -0
- package/dist/src/config-schema.js +133 -0
- package/dist/src/deliver.js +126 -0
- package/dist/src/dispatch.js +186 -0
- package/dist/src/format.js +31 -0
- package/dist/src/inbound-attachments.js +125 -0
- package/dist/src/inbound.js +362 -0
- package/dist/src/media-temp.js +92 -0
- package/dist/src/model-buttons.js +96 -0
- package/dist/src/monitor-types.js +27 -0
- package/dist/src/monitor.js +40 -0
- package/dist/src/onboarding.js +180 -0
- package/dist/src/polling.js +98 -0
- package/dist/src/presentation.js +374 -0
- package/dist/src/runtime.js +36 -0
- package/dist/src/russian-trusted-ca.js +84 -0
- package/dist/src/send.js +612 -0
- package/dist/src/state.js +111 -0
- package/dist/src/sticker-cache.js +26 -0
- package/dist/src/stream-draft.js +102 -0
- package/dist/src/types.js +5 -0
- package/dist/src/webhook-queue.js +215 -0
- package/dist/src/webhook-runner.js +233 -0
- package/dist/src/webhook.js +196 -0
- package/openclaw.plugin.json +263 -0
- package/package.json +100 -0
- package/scripts/check-cycles.mjs +38 -0
- package/scripts/test-api.mjs +169 -0
- package/scripts/test-send.mjs +162 -0
- package/scripts/update-schema.mjs +48 -0
- package/src/accounts.ts +193 -0
- package/src/actions.ts +501 -0
- package/src/api.ts +758 -0
- package/src/callbacks.ts +140 -0
- package/src/channel-agent-prompt.ts +47 -0
- package/src/channel-config.ts +138 -0
- package/src/channel-directory.ts +90 -0
- package/src/channel-lifecycle.ts +256 -0
- package/src/channel-outbound.ts +159 -0
- package/src/channel-policy.ts +185 -0
- package/src/channel.ts +99 -0
- package/src/config-schema.ts +154 -0
- package/src/deliver.ts +168 -0
- package/src/dispatch.ts +224 -0
- package/src/format.ts +33 -0
- package/src/inbound-attachments.ts +152 -0
- package/src/inbound.ts +412 -0
- package/src/media-temp.ts +103 -0
- package/src/model-buttons.ts +150 -0
- package/src/monitor-types.ts +61 -0
- package/src/monitor.ts +49 -0
- package/src/onboarding.ts +215 -0
- package/src/polling.ts +125 -0
- package/src/presentation.ts +452 -0
- package/src/runtime.ts +56 -0
- package/src/russian-trusted-ca.ts +86 -0
- package/src/send.ts +774 -0
- package/src/state.ts +149 -0
- package/src/sticker-cache.ts +28 -0
- package/src/stream-draft.ts +125 -0
- package/src/types.ts +263 -0
- package/src/webhook-queue.ts +248 -0
- package/src/webhook-runner.ts +296 -0
- package/src/webhook.ts +268 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagePresentation → MAX message (text in MAX markdown + inline_keyboard).
|
|
3
|
+
*
|
|
4
|
+
* Contract: docs/plugins/message-presentation.md. Core adapts a presentation to
|
|
5
|
+
* `MAX_PRESENTATION_CAPABILITIES` and calls `renderMaxPresentation`; the result
|
|
6
|
+
* is an ordinary payload with `channelData.max.buttons`, sent by sendPayload.
|
|
7
|
+
* Channel-local funnels (agent replies, the message tool) go through the same
|
|
8
|
+
* `renderPresentationForDelivery` so they behave identically.
|
|
9
|
+
*
|
|
10
|
+
* Presentation actions become transport-private callback payloads (≤1024
|
|
11
|
+
* bytes) that `decodeMaxPresentationCallback` maps back on `message_callback`:
|
|
12
|
+
* - command → the command text itself ("/status"): it re-enters as a user
|
|
13
|
+
* message and takes core's native command path;
|
|
14
|
+
* - callback → "mxcb1:<value>": opaque, delivered to the agent as
|
|
15
|
+
* "callback_data: <value>" and never parsed as a slash command;
|
|
16
|
+
* - approval → "mxa1:<e|p|s>:<o|a|d>:<approvalId>": resolved through the
|
|
17
|
+
* canonical approval service (approval-gateway-runtime);
|
|
18
|
+
* - question → "mxq1:<questionId>:<optionValue>": resolved through
|
|
19
|
+
* question-gateway-runtime;
|
|
20
|
+
* - url / URL-backed web-app → a MAX `link` button.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ChannelOutboundAdapter } from 'openclaw/plugin-sdk/channel-contract';
|
|
24
|
+
import type { ReplyPayload } from 'openclaw/plugin-sdk/core';
|
|
25
|
+
import {
|
|
26
|
+
type MessagePresentation,
|
|
27
|
+
type MessagePresentationBlock,
|
|
28
|
+
type MessagePresentationButton,
|
|
29
|
+
type MessagePresentationOption,
|
|
30
|
+
renderMessagePresentationFallbackText,
|
|
31
|
+
renderPresentationForDelivery,
|
|
32
|
+
type ReplyPayloadDelivery,
|
|
33
|
+
resolveMessagePresentationButtonAction,
|
|
34
|
+
resolveMessagePresentationOptionAction,
|
|
35
|
+
} from 'openclaw/plugin-sdk/interactive-runtime';
|
|
36
|
+
|
|
37
|
+
import type { MaxSendButton } from './send.js';
|
|
38
|
+
|
|
39
|
+
type ChannelPresentationCapabilities = NonNullable<
|
|
40
|
+
ChannelOutboundAdapter['presentationCapabilities']
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/** MAX Bot API limits (schema.yaml: Button.text, CallbackButton.payload, InlineKeyboard). */
|
|
44
|
+
export const MAX_BUTTON_TEXT_LIMIT = 128;
|
|
45
|
+
export const MAX_CALLBACK_PAYLOAD_BYTES = 1024;
|
|
46
|
+
export const MAX_KEYBOARD_ROWS = 30;
|
|
47
|
+
export const MAX_KEYBOARD_BUTTONS = 210;
|
|
48
|
+
export const MAX_LINK_URL_LENGTH = 2048;
|
|
49
|
+
export const MAX_TEXT_LIMIT = 4000;
|
|
50
|
+
/** Buttons per row we render: within MAX's 7 (3 for link/open_app/request_*). */
|
|
51
|
+
const BUTTONS_PER_ROW = 3;
|
|
52
|
+
const SELECT_OPTIONS_PER_ROW = 2;
|
|
53
|
+
|
|
54
|
+
export const MAX_PRESENTATION_CAPABILITIES: ChannelPresentationCapabilities = {
|
|
55
|
+
supported: true,
|
|
56
|
+
buttons: true,
|
|
57
|
+
selects: true,
|
|
58
|
+
context: true,
|
|
59
|
+
divider: true,
|
|
60
|
+
// Rendered as monospace text blocks (MAX has no native charts/tables).
|
|
61
|
+
charts: true,
|
|
62
|
+
tables: true,
|
|
63
|
+
limits: {
|
|
64
|
+
actions: {
|
|
65
|
+
maxActions: MAX_KEYBOARD_BUTTONS,
|
|
66
|
+
maxActionsPerRow: BUTTONS_PER_ROW,
|
|
67
|
+
maxRows: MAX_KEYBOARD_ROWS,
|
|
68
|
+
maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
|
|
69
|
+
// Leave room for the private envelope prefix inside the 1024-byte payload.
|
|
70
|
+
maxValueBytes: 960,
|
|
71
|
+
supportsStyles: false,
|
|
72
|
+
supportsDisabled: false,
|
|
73
|
+
},
|
|
74
|
+
selects: {
|
|
75
|
+
maxOptions: 20,
|
|
76
|
+
maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
|
|
77
|
+
maxValueBytes: 960,
|
|
78
|
+
},
|
|
79
|
+
text: {
|
|
80
|
+
maxLength: MAX_TEXT_LIMIT,
|
|
81
|
+
encoding: 'characters',
|
|
82
|
+
markdownDialect: 'markdown',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ── Callback envelopes ──
|
|
88
|
+
|
|
89
|
+
const CALLBACK_PREFIX = 'mxcb1:';
|
|
90
|
+
const APPROVAL_PREFIX = 'mxa1:';
|
|
91
|
+
const QUESTION_PREFIX = 'mxq1:';
|
|
92
|
+
|
|
93
|
+
type ApprovalKind = 'exec' | 'plugin' | 'system-agent';
|
|
94
|
+
type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
|
|
95
|
+
|
|
96
|
+
const APPROVAL_KIND_CODES: Record<ApprovalKind, string> = {
|
|
97
|
+
exec: 'e',
|
|
98
|
+
plugin: 'p',
|
|
99
|
+
'system-agent': 's',
|
|
100
|
+
};
|
|
101
|
+
const APPROVAL_DECISION_CODES: Record<ApprovalDecision, string> = {
|
|
102
|
+
'allow-once': 'o',
|
|
103
|
+
'allow-always': 'a',
|
|
104
|
+
deny: 'd',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type MaxPresentationCallback =
|
|
108
|
+
| { kind: 'callback'; value: string }
|
|
109
|
+
| { kind: 'approval'; approvalId: string; approvalKind: ApprovalKind; decision: ApprovalDecision }
|
|
110
|
+
| { kind: 'question'; questionId: string; optionValue: string };
|
|
111
|
+
|
|
112
|
+
function fitsPayload(payload: string): boolean {
|
|
113
|
+
return payload.length > 0 && Buffer.byteLength(payload, 'utf8') <= MAX_CALLBACK_PAYLOAD_BYTES;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Decode a payload produced by this renderer; null for anything else (legacy buttons). */
|
|
117
|
+
export function decodeMaxPresentationCallback(
|
|
118
|
+
payload: string | undefined | null,
|
|
119
|
+
): MaxPresentationCallback | null {
|
|
120
|
+
if (!payload) return null;
|
|
121
|
+
if (payload.startsWith(CALLBACK_PREFIX)) {
|
|
122
|
+
const value = payload.slice(CALLBACK_PREFIX.length);
|
|
123
|
+
return value ? { kind: 'callback', value } : null;
|
|
124
|
+
}
|
|
125
|
+
if (payload.startsWith(APPROVAL_PREFIX)) {
|
|
126
|
+
const match = /^mxa1:([eps]):([oad]):(.+)$/su.exec(payload);
|
|
127
|
+
if (!match) return null;
|
|
128
|
+
const approvalKind = (Object.keys(APPROVAL_KIND_CODES) as ApprovalKind[]).find(
|
|
129
|
+
(kind) => APPROVAL_KIND_CODES[kind] === match[1],
|
|
130
|
+
);
|
|
131
|
+
const decision = (Object.keys(APPROVAL_DECISION_CODES) as ApprovalDecision[]).find(
|
|
132
|
+
(code) => APPROVAL_DECISION_CODES[code] === match[2],
|
|
133
|
+
);
|
|
134
|
+
return approvalKind && decision
|
|
135
|
+
? { kind: 'approval', approvalKind, decision, approvalId: match[3] }
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
if (payload.startsWith(QUESTION_PREFIX)) {
|
|
139
|
+
const rest = payload.slice(QUESTION_PREFIX.length);
|
|
140
|
+
const separator = rest.indexOf(':');
|
|
141
|
+
if (separator <= 0 || separator === rest.length - 1) return null;
|
|
142
|
+
return {
|
|
143
|
+
kind: 'question',
|
|
144
|
+
questionId: rest.slice(0, separator),
|
|
145
|
+
optionValue: rest.slice(separator + 1),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Controls ──
|
|
152
|
+
|
|
153
|
+
type ControlResult = { button: MaxSendButton } | { dropped: string };
|
|
154
|
+
|
|
155
|
+
function clipLabel(label: string): string {
|
|
156
|
+
const chars = Array.from(label.trim());
|
|
157
|
+
return chars.length <= MAX_BUTTON_TEXT_LIMIT
|
|
158
|
+
? chars.join('')
|
|
159
|
+
: `${chars.slice(0, MAX_BUTTON_TEXT_LIMIT - 1).join('')}…`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function callbackButton(label: string, payload: string): ControlResult {
|
|
163
|
+
return fitsPayload(payload)
|
|
164
|
+
? { button: { text: clipLabel(label), type: 'callback', payload } }
|
|
165
|
+
: { dropped: label };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function linkButton(label: string, url: string): ControlResult {
|
|
169
|
+
const trimmed = url.trim();
|
|
170
|
+
if (!/^https?:\/\//iu.test(trimmed) || trimmed.length > MAX_LINK_URL_LENGTH)
|
|
171
|
+
return { dropped: label };
|
|
172
|
+
return { button: { text: clipLabel(label), type: 'link', url: trimmed } };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function commandPayload(command: string): string {
|
|
176
|
+
const trimmed = command.trim();
|
|
177
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toControl(button: MessagePresentationButton): ControlResult {
|
|
181
|
+
const label = button.label;
|
|
182
|
+
const action = resolveMessagePresentationButtonAction(button);
|
|
183
|
+
if (!action || button.disabled) return { dropped: label };
|
|
184
|
+
switch (action.type) {
|
|
185
|
+
case 'url':
|
|
186
|
+
return linkButton(label, action.url);
|
|
187
|
+
case 'web-app':
|
|
188
|
+
// MAX open_app addresses a bot's mini app by name, not by URL.
|
|
189
|
+
return action.url ? linkButton(label, action.url) : { dropped: label };
|
|
190
|
+
case 'command':
|
|
191
|
+
return action.command.trim()
|
|
192
|
+
? callbackButton(label, commandPayload(action.command))
|
|
193
|
+
: { dropped: label };
|
|
194
|
+
case 'callback':
|
|
195
|
+
return callbackButton(label, `${CALLBACK_PREFIX}${action.value}`);
|
|
196
|
+
case 'approval': {
|
|
197
|
+
const kind = APPROVAL_KIND_CODES[action.approvalKind as ApprovalKind];
|
|
198
|
+
const decision = APPROVAL_DECISION_CODES[action.decision as ApprovalDecision];
|
|
199
|
+
if (!kind || !decision || !action.approvalId) return { dropped: label };
|
|
200
|
+
return callbackButton(label, `${APPROVAL_PREFIX}${kind}:${decision}:${action.approvalId}`);
|
|
201
|
+
}
|
|
202
|
+
case 'question':
|
|
203
|
+
// custom-input needs a free-text composer target MAX cannot address; the
|
|
204
|
+
// producer states the text route in the message, so the control is omitted.
|
|
205
|
+
if ('intent' in action || action.questionId.includes(':')) return { dropped: label };
|
|
206
|
+
return callbackButton(label, `${QUESTION_PREFIX}${action.questionId}:${action.optionValue}`);
|
|
207
|
+
default:
|
|
208
|
+
return { dropped: label };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function toOptionControl(option: MessagePresentationOption): ControlResult {
|
|
213
|
+
const action = resolveMessagePresentationOptionAction(option);
|
|
214
|
+
if (!action) return { dropped: option.label };
|
|
215
|
+
return action.type === 'command'
|
|
216
|
+
? callbackButton(option.label, commandPayload(action.command))
|
|
217
|
+
: callbackButton(option.label, `${CALLBACK_PREFIX}${action.value}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Text ──
|
|
221
|
+
|
|
222
|
+
const TONE_PREFIX: Record<string, string> = {
|
|
223
|
+
info: 'ℹ️',
|
|
224
|
+
success: '✅',
|
|
225
|
+
warning: '⚠️',
|
|
226
|
+
danger: '⛔',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function collapse(value: string | number): string {
|
|
230
|
+
return String(value).replace(/\s+/gu, ' ').replace(/`/gu, "'").trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function monospaceTable(headers: string[], rows: string[][]): string {
|
|
234
|
+
const widths = headers.map((header, column) =>
|
|
235
|
+
Math.max(Array.from(header).length, ...rows.map((row) => Array.from(row[column] ?? '').length)),
|
|
236
|
+
);
|
|
237
|
+
const pad = (cells: string[]) =>
|
|
238
|
+
cells
|
|
239
|
+
.map(
|
|
240
|
+
(cell, column) => cell + ' '.repeat(Math.max(0, widths[column] - Array.from(cell).length)),
|
|
241
|
+
)
|
|
242
|
+
.join(' | ')
|
|
243
|
+
.trimEnd();
|
|
244
|
+
const lines = [
|
|
245
|
+
pad(headers),
|
|
246
|
+
widths.map((width) => '-'.repeat(Math.max(1, width))).join('-+-'),
|
|
247
|
+
...rows.map(pad),
|
|
248
|
+
];
|
|
249
|
+
return ['```', ...lines, '```'].join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderDataBlock(
|
|
253
|
+
block: Extract<MessagePresentationBlock, { type: 'table' | 'chart' }>,
|
|
254
|
+
): string {
|
|
255
|
+
if (block.type === 'table') {
|
|
256
|
+
return [
|
|
257
|
+
`**${collapse(block.caption)}**`,
|
|
258
|
+
monospaceTable(
|
|
259
|
+
block.headers.map(collapse),
|
|
260
|
+
block.rows.map((row) => row.map(collapse)),
|
|
261
|
+
),
|
|
262
|
+
].join('\n');
|
|
263
|
+
}
|
|
264
|
+
if (block.chartType === 'pie') {
|
|
265
|
+
const total = block.segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
266
|
+
const rows = block.segments.map((segment) => [
|
|
267
|
+
collapse(segment.label),
|
|
268
|
+
collapse(segment.value),
|
|
269
|
+
total > 0 ? `${((segment.value / total) * 100).toFixed(1)}%` : '',
|
|
270
|
+
]);
|
|
271
|
+
return [`**${collapse(block.title)}** (pie)`, monospaceTable(['', '', '%'], rows)].join('\n');
|
|
272
|
+
}
|
|
273
|
+
const headers = [
|
|
274
|
+
collapse(block.xLabel ?? ''),
|
|
275
|
+
...block.series.map((series) => collapse(series.name)),
|
|
276
|
+
];
|
|
277
|
+
const rows = block.categories.map((category, index) => [
|
|
278
|
+
collapse(category),
|
|
279
|
+
...block.series.map((series) => collapse(series.values[index] ?? '')),
|
|
280
|
+
]);
|
|
281
|
+
const axis = block.yLabel ? `, ${collapse(block.yLabel)}` : '';
|
|
282
|
+
return [
|
|
283
|
+
`**${collapse(block.title)}** (${block.chartType}${axis})`,
|
|
284
|
+
monospaceTable(headers, rows),
|
|
285
|
+
].join('\n');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderContext(text: string): string {
|
|
289
|
+
const trimmed = text.trim();
|
|
290
|
+
return /[_*\n]/u.test(trimmed) ? trimmed : `_${trimmed}_`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function chunk<T>(items: T[], size: number): T[][] {
|
|
294
|
+
const rows: T[][] = [];
|
|
295
|
+
for (let index = 0; index < items.length; index += size)
|
|
296
|
+
rows.push(items.slice(index, index + size));
|
|
297
|
+
return rows;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export type MaxRenderedPresentation = {
|
|
301
|
+
text: string;
|
|
302
|
+
buttons: MaxSendButton[][];
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** Pure mapping of an (already adapted) presentation to MAX text + keyboard rows. */
|
|
306
|
+
export function renderMaxPresentationParts(params: {
|
|
307
|
+
presentation: MessagePresentation;
|
|
308
|
+
text?: string | null;
|
|
309
|
+
}): MaxRenderedPresentation {
|
|
310
|
+
const { presentation } = params;
|
|
311
|
+
const baseText = params.text?.trim() ?? '';
|
|
312
|
+
const parts: string[] = [];
|
|
313
|
+
|
|
314
|
+
const title = presentation.title?.trim();
|
|
315
|
+
if (title && !baseText.startsWith(title)) {
|
|
316
|
+
const tone = presentation.tone ? TONE_PREFIX[presentation.tone] : undefined;
|
|
317
|
+
parts.push(tone ? `${tone} **${title}**` : `**${title}**`);
|
|
318
|
+
}
|
|
319
|
+
if (baseText) parts.push(baseText);
|
|
320
|
+
|
|
321
|
+
const rows: MaxSendButton[][] = [];
|
|
322
|
+
const dropped: string[] = [];
|
|
323
|
+
let buttonCount = 0;
|
|
324
|
+
const pushRows = (candidates: MaxSendButton[][]) => {
|
|
325
|
+
for (const row of candidates) {
|
|
326
|
+
const room = Math.min(row.length, MAX_KEYBOARD_BUTTONS - buttonCount);
|
|
327
|
+
if (rows.length >= MAX_KEYBOARD_ROWS || room <= 0) {
|
|
328
|
+
dropped.push(...row.map((button) => button.text));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
rows.push(row.slice(0, room));
|
|
332
|
+
dropped.push(...row.slice(room).map((button) => button.text));
|
|
333
|
+
buttonCount += room;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const block of presentation.blocks) {
|
|
338
|
+
switch (block.type) {
|
|
339
|
+
case 'text':
|
|
340
|
+
if (block.text.trim()) parts.push(block.text.trim());
|
|
341
|
+
break;
|
|
342
|
+
case 'context':
|
|
343
|
+
if (block.text.trim()) parts.push(renderContext(block.text));
|
|
344
|
+
break;
|
|
345
|
+
case 'divider':
|
|
346
|
+
parts.push('———');
|
|
347
|
+
break;
|
|
348
|
+
case 'table':
|
|
349
|
+
case 'chart':
|
|
350
|
+
parts.push(renderDataBlock(block));
|
|
351
|
+
break;
|
|
352
|
+
case 'buttons': {
|
|
353
|
+
const buttons: MaxSendButton[] = [];
|
|
354
|
+
for (const control of block.buttons.map(toControl)) {
|
|
355
|
+
if ('button' in control) buttons.push(control.button);
|
|
356
|
+
else dropped.push(control.dropped);
|
|
357
|
+
}
|
|
358
|
+
pushRows(chunk(buttons, BUTTONS_PER_ROW));
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case 'select': {
|
|
362
|
+
const options: MaxSendButton[] = [];
|
|
363
|
+
for (const control of block.options.map(toOptionControl)) {
|
|
364
|
+
if ('button' in control) options.push(control.button);
|
|
365
|
+
else dropped.push(control.dropped);
|
|
366
|
+
}
|
|
367
|
+
pushRows(chunk(options, SELECT_OPTIONS_PER_ROW));
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (dropped.length > 0) {
|
|
374
|
+
// Label-only fallback, as core renders controls a channel cannot carry.
|
|
375
|
+
const fallback = renderMessagePresentationFallbackText({
|
|
376
|
+
presentation: {
|
|
377
|
+
blocks: [
|
|
378
|
+
{ type: 'buttons', buttons: dropped.map((label) => ({ label, value: 'unavailable' })) },
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
if (fallback) parts.push(fallback);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { text: parts.join('\n\n'), buttons: rows };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function readChannelDataMax(channelData: unknown): Record<string, unknown> {
|
|
389
|
+
if (!channelData || typeof channelData !== 'object' || Array.isArray(channelData)) return {};
|
|
390
|
+
const max = (channelData as Record<string, unknown>).max;
|
|
391
|
+
return max && typeof max === 'object' && !Array.isArray(max)
|
|
392
|
+
? (max as Record<string, unknown>)
|
|
393
|
+
: {};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** `renderPresentation` for the outbound adapter: presentation → channelData.max.buttons. */
|
|
397
|
+
export function renderMaxPresentation(
|
|
398
|
+
payload: ReplyPayload,
|
|
399
|
+
presentation: MessagePresentation,
|
|
400
|
+
): ReplyPayload {
|
|
401
|
+
const rendered = renderMaxPresentationParts({ presentation, text: payload.text });
|
|
402
|
+
const existingMax = readChannelDataMax(payload.channelData);
|
|
403
|
+
const existingButtons = Array.isArray(existingMax.buttons)
|
|
404
|
+
? (existingMax.buttons as MaxSendButton[][])
|
|
405
|
+
: [];
|
|
406
|
+
const buttons = [...existingButtons, ...rendered.buttons];
|
|
407
|
+
const rest: ReplyPayload = { ...payload };
|
|
408
|
+
delete rest.presentation;
|
|
409
|
+
return {
|
|
410
|
+
...rest,
|
|
411
|
+
text: rendered.text,
|
|
412
|
+
...(buttons.length > 0 || payload.channelData
|
|
413
|
+
? {
|
|
414
|
+
channelData: {
|
|
415
|
+
...(payload.channelData ?? {}),
|
|
416
|
+
max: { ...existingMax, ...(buttons.length > 0 ? { buttons } : {}) },
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
: {}),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Same adaptation + render + fallback policy core applies on its outbound path. */
|
|
424
|
+
export async function materializeMaxPresentation(payload: ReplyPayload): Promise<ReplyPayload> {
|
|
425
|
+
if (!payload.presentation) return payload;
|
|
426
|
+
return renderPresentationForDelivery(
|
|
427
|
+
{
|
|
428
|
+
presentationCapabilities: MAX_PRESENTATION_CAPABILITIES,
|
|
429
|
+
renderPresentation: (adapted) => renderMaxPresentation(adapted, adapted.presentation),
|
|
430
|
+
},
|
|
431
|
+
payload,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export type MaxDeliveryPin = { enabled: boolean; notify?: boolean; required?: boolean };
|
|
436
|
+
|
|
437
|
+
/** Normalize `delivery.pin` (true | {enabled, notify, required}) or the tool's `pin: true`. */
|
|
438
|
+
export function readMaxDeliveryPin(
|
|
439
|
+
delivery: unknown,
|
|
440
|
+
pinFlag?: unknown,
|
|
441
|
+
): MaxDeliveryPin | undefined {
|
|
442
|
+
const raw =
|
|
443
|
+
delivery && typeof delivery === 'object' && !Array.isArray(delivery)
|
|
444
|
+
? (delivery as ReplyPayloadDelivery).pin
|
|
445
|
+
: undefined;
|
|
446
|
+
if (raw === true) return { enabled: true };
|
|
447
|
+
if (raw && typeof raw === 'object' && raw.enabled) {
|
|
448
|
+
return { enabled: true, notify: raw.notify === true, required: raw.required === true };
|
|
449
|
+
}
|
|
450
|
+
if (pinFlag === true) return { enabled: true };
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setMaxRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getMaxRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error('MAX runtime not initialized');
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Legacy runtime config API (OpenClaw < 2026.9.3).
|
|
18
|
+
* Since 2026.9.3 `loadConfig`/`writeConfigFile` are removed from PluginRuntime;
|
|
19
|
+
* the runtime exposes `config.current()` and transactional `replaceConfigFile`/`mutateConfigFile`.
|
|
20
|
+
*/
|
|
21
|
+
type LegacyMaxRuntimeConfigApi = {
|
|
22
|
+
loadConfig?: () => Promise<OpenClawConfig> | OpenClawConfig;
|
|
23
|
+
writeConfigFile?: (cfg: OpenClawConfig) => Promise<unknown> | unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Read the current config snapshot through the plugin runtime, on any supported OpenClaw version. */
|
|
27
|
+
export async function loadMaxConfig(): Promise<OpenClawConfig> {
|
|
28
|
+
const config = getMaxRuntime().config;
|
|
29
|
+
if (typeof config.current === 'function') {
|
|
30
|
+
return config.current() as OpenClawConfig;
|
|
31
|
+
}
|
|
32
|
+
const legacy = config as unknown as LegacyMaxRuntimeConfigApi;
|
|
33
|
+
if (typeof legacy.loadConfig === 'function') {
|
|
34
|
+
return await legacy.loadConfig();
|
|
35
|
+
}
|
|
36
|
+
throw new Error(
|
|
37
|
+
'MAX runtime config API unavailable: neither config.current() nor config.loadConfig()',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Persist a full config replacement through the plugin runtime, on any supported OpenClaw version. */
|
|
42
|
+
export async function writeMaxConfig(nextConfig: OpenClawConfig): Promise<void> {
|
|
43
|
+
const config = getMaxRuntime().config;
|
|
44
|
+
if (typeof config.replaceConfigFile === 'function') {
|
|
45
|
+
await config.replaceConfigFile({ nextConfig, afterWrite: { mode: 'auto' } });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const legacy = config as unknown as LegacyMaxRuntimeConfigApi;
|
|
49
|
+
if (typeof legacy.writeConfigFile === 'function') {
|
|
50
|
+
await legacy.writeConfigFile(nextConfig);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(
|
|
54
|
+
'MAX runtime config API unavailable: neither config.replaceConfigFile() nor config.writeConfigFile()',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Russian Trusted Root/Sub CA (Минцифры РФ) — required for TLS to platform-api2.max.ru.
|
|
3
|
+
*
|
|
4
|
+
* Source: https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
|
|
5
|
+
* https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt
|
|
6
|
+
* Root CA valid until 2032-02-27, Sub CA until 2027-03-06.
|
|
7
|
+
* These are used ONLY for MAX API connections (per-dispatcher ca), never installed process-wide.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const RUSSIAN_TRUSTED_ROOT_CA = `
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
|
13
|
+
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
|
14
|
+
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
|
15
|
+
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
|
|
16
|
+
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
|
17
|
+
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
|
|
18
|
+
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
|
|
19
|
+
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
|
|
20
|
+
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
|
|
21
|
+
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
|
|
22
|
+
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
|
|
23
|
+
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
|
|
24
|
+
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
|
|
25
|
+
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
|
|
26
|
+
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
|
|
27
|
+
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
|
|
28
|
+
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
|
|
29
|
+
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
|
|
30
|
+
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
|
|
31
|
+
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
|
|
32
|
+
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
|
|
33
|
+
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
|
|
34
|
+
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
|
|
35
|
+
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
|
|
36
|
+
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
|
|
37
|
+
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
|
|
38
|
+
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
|
|
39
|
+
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
|
|
40
|
+
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
|
|
41
|
+
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
|
|
42
|
+
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
|
|
43
|
+
-----END CERTIFICATE-----`;
|
|
44
|
+
|
|
45
|
+
export const RUSSIAN_TRUSTED_SUB_CA = `
|
|
46
|
+
-----BEGIN CERTIFICATE-----
|
|
47
|
+
MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
|
48
|
+
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
|
49
|
+
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
|
50
|
+
Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
|
|
51
|
+
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
|
52
|
+
YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
|
|
53
|
+
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
|
|
54
|
+
wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
|
|
55
|
+
HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
|
|
56
|
+
S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
|
|
57
|
+
vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
|
|
58
|
+
BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
|
|
59
|
+
vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
|
|
60
|
+
L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
|
|
61
|
+
3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
|
|
62
|
+
qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
|
|
63
|
+
Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
|
|
64
|
+
2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
|
|
65
|
+
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
|
|
66
|
+
XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
|
|
67
|
+
AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
|
|
68
|
+
b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
|
|
69
|
+
eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
|
|
70
|
+
aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
|
|
71
|
+
gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
|
|
72
|
+
b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
|
|
73
|
+
L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
|
|
74
|
+
LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
|
|
75
|
+
AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
|
|
76
|
+
Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
|
|
77
|
+
anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
|
|
78
|
+
cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
|
|
79
|
+
Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
|
|
80
|
+
Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
|
|
81
|
+
h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
|
|
82
|
+
F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
|
|
83
|
+
E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
|
|
84
|
+
GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
|
|
85
|
+
ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
|
|
86
|
+
-----END CERTIFICATE-----`;
|