@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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound adapter of the MAX channel: sendText / sendPayload / sendMedia,
|
|
3
|
+
* presentation rendering and delivery pins for core's outbound path.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ChannelPlugin } from 'openclaw/plugin-sdk/channel-core';
|
|
7
|
+
|
|
8
|
+
import { type ResolvedMaxAccount, resolveMaxAccount } from './accounts.js';
|
|
9
|
+
import {
|
|
10
|
+
materializeMaxPresentation,
|
|
11
|
+
MAX_PRESENTATION_CAPABILITIES,
|
|
12
|
+
MAX_TEXT_LIMIT,
|
|
13
|
+
renderMaxPresentation,
|
|
14
|
+
} from './presentation.js';
|
|
15
|
+
import { getMaxRuntime, loadMaxConfig } from './runtime.js';
|
|
16
|
+
import {
|
|
17
|
+
pinMaxMessage,
|
|
18
|
+
readMaxChannelButtons,
|
|
19
|
+
sendMaxMediaGroup,
|
|
20
|
+
sendMaxMediaMessage,
|
|
21
|
+
sendMaxMessage,
|
|
22
|
+
} from './send.js';
|
|
23
|
+
|
|
24
|
+
type MaxChannelPlugin = ChannelPlugin<ResolvedMaxAccount>;
|
|
25
|
+
|
|
26
|
+
/** Direct delivery with 4000-character markdown chunks. */
|
|
27
|
+
export const maxOutboundAdapter: NonNullable<MaxChannelPlugin['outbound']> = {
|
|
28
|
+
deliveryMode: 'direct',
|
|
29
|
+
chunker: (text, limit) => getMaxRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
30
|
+
chunkerMode: 'markdown',
|
|
31
|
+
textChunkLimit: 4000,
|
|
32
|
+
|
|
33
|
+
// Presentation (docs/plugins/message-presentation.md): core adapts to these
|
|
34
|
+
// capabilities, renders through renderPresentation and pins through
|
|
35
|
+
// pinDeliveredMessage after the first delivered message.
|
|
36
|
+
presentationCapabilities: MAX_PRESENTATION_CAPABILITIES,
|
|
37
|
+
deliveryCapabilities: { pin: true },
|
|
38
|
+
renderPresentation: ({ payload, presentation }) => renderMaxPresentation(payload, presentation),
|
|
39
|
+
pinDeliveredMessage: async ({ cfg, target, messageId, pin, assertDirectAdapterHandoff }) => {
|
|
40
|
+
const account = resolveMaxAccount({ cfg, accountId: target.accountId });
|
|
41
|
+
if (!account.token) throw new Error('MAX bot token not configured');
|
|
42
|
+
assertDirectAdapterHandoff?.();
|
|
43
|
+
// Dialogs cannot pin: pinMaxMessage resolves { pinned: false } without an
|
|
44
|
+
// API call or error, so core logs no failed pin for a DM delivery.
|
|
45
|
+
await pinMaxMessage(target.to, messageId, {
|
|
46
|
+
token: account.token,
|
|
47
|
+
pinNotify: pin.notify === true,
|
|
48
|
+
beforeRequest: assertDirectAdapterHandoff,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
sendPayload: async ({ to, text, payload: rawPayload, mediaUrl, accountId, replyToId }) => {
|
|
53
|
+
const cfg = await loadMaxConfig();
|
|
54
|
+
const account = resolveMaxAccount({ cfg, accountId });
|
|
55
|
+
if (!account.token) throw new Error('MAX bot token not configured');
|
|
56
|
+
|
|
57
|
+
// Core renders presentation before sendPayload; a payload that still
|
|
58
|
+
// carries one came through a path that did not, so apply the same policy.
|
|
59
|
+
const payload = rawPayload.presentation
|
|
60
|
+
? await materializeMaxPresentation(rawPayload)
|
|
61
|
+
: rawPayload;
|
|
62
|
+
const effectiveText = payload === rawPayload ? text : (payload.text ?? '');
|
|
63
|
+
const buttons = readMaxChannelButtons(payload.channelData);
|
|
64
|
+
const mediaUrls = mediaUrl
|
|
65
|
+
? [mediaUrl]
|
|
66
|
+
: payload.mediaUrls?.length
|
|
67
|
+
? payload.mediaUrls
|
|
68
|
+
: payload.mediaUrl
|
|
69
|
+
? [payload.mediaUrl]
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
if (mediaUrls.length) {
|
|
73
|
+
// Albums: up to 12 images/videos per message, caption on the first,
|
|
74
|
+
// buttons on the last; report the first message (delivery.pin).
|
|
75
|
+
const result = await sendMaxMediaGroup(to, effectiveText, mediaUrls, {
|
|
76
|
+
token: account.token,
|
|
77
|
+
replyToMessageId: replyToId ?? undefined,
|
|
78
|
+
format: 'markdown',
|
|
79
|
+
buttons,
|
|
80
|
+
mediaMaxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
channel: 'max',
|
|
84
|
+
messageId: result.messageIds[0] ?? '',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MAX caps a message at 4000 characters: split, keyboard on the last
|
|
89
|
+
// chunk, report the first one (delivery.pin pins the first chunk).
|
|
90
|
+
const chunks =
|
|
91
|
+
effectiveText.length > MAX_TEXT_LIMIT
|
|
92
|
+
? getMaxRuntime().channel.text.chunkMarkdownText(effectiveText, MAX_TEXT_LIMIT)
|
|
93
|
+
: [effectiveText];
|
|
94
|
+
let firstMessageId = '';
|
|
95
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
96
|
+
const result = await sendMaxMessage(to, chunks[index], {
|
|
97
|
+
token: account.token,
|
|
98
|
+
replyToMessageId: index === 0 ? (replyToId ?? undefined) : undefined,
|
|
99
|
+
format: 'markdown',
|
|
100
|
+
buttons: index === chunks.length - 1 ? buttons : undefined,
|
|
101
|
+
});
|
|
102
|
+
if (index === 0) firstMessageId = result.messageId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
channel: 'max',
|
|
107
|
+
messageId: firstMessageId,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
sendText: async ({ to, text, accountId, replyToId }) => {
|
|
112
|
+
const cfg = await loadMaxConfig();
|
|
113
|
+
const account = resolveMaxAccount({ cfg, accountId });
|
|
114
|
+
if (!account.token) throw new Error('MAX bot token not configured');
|
|
115
|
+
|
|
116
|
+
const result = await sendMaxMessage(to, text, {
|
|
117
|
+
token: account.token,
|
|
118
|
+
replyToMessageId: replyToId ?? undefined,
|
|
119
|
+
format: 'markdown',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
channel: 'max',
|
|
124
|
+
messageId: result.messageId,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => {
|
|
129
|
+
const cfg = await loadMaxConfig();
|
|
130
|
+
const account = resolveMaxAccount({ cfg, accountId });
|
|
131
|
+
if (!account.token) throw new Error('MAX bot token not configured');
|
|
132
|
+
|
|
133
|
+
if (!mediaUrl) {
|
|
134
|
+
// No media, send as text
|
|
135
|
+
const result = await sendMaxMessage(to, text, {
|
|
136
|
+
token: account.token,
|
|
137
|
+
replyToMessageId: replyToId ?? undefined,
|
|
138
|
+
format: 'markdown',
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
channel: 'max',
|
|
142
|
+
messageId: result.messageId,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Upload and send media
|
|
147
|
+
const result = await sendMaxMediaMessage(to, text, mediaUrl, {
|
|
148
|
+
token: account.token,
|
|
149
|
+
replyToMessageId: replyToId ?? undefined,
|
|
150
|
+
format: 'markdown',
|
|
151
|
+
mediaMaxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
channel: 'max',
|
|
156
|
+
messageId: result.messageId,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access policy of the MAX channel: DM pairing/allowlist, group
|
|
3
|
+
* requireMention and tool policy, security warnings, pairing approval.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ChannelPlugin } from 'openclaw/plugin-sdk/channel-core';
|
|
7
|
+
import type { GroupToolPolicyConfig } from 'openclaw/plugin-sdk/channel-policy';
|
|
8
|
+
import { resolveToolsBySender } from 'openclaw/plugin-sdk/channel-policy';
|
|
9
|
+
import { PAIRING_APPROVED_MESSAGE } from 'openclaw/plugin-sdk/channel-status';
|
|
10
|
+
import type { OpenClawConfig } from 'openclaw/plugin-sdk/core';
|
|
11
|
+
import { DEFAULT_ACCOUNT_ID, formatPairingApproveHint } from 'openclaw/plugin-sdk/core';
|
|
12
|
+
|
|
13
|
+
import { type ResolvedMaxAccount, resolveMaxAccount } from './accounts.js';
|
|
14
|
+
import { sendMaxMessage } from './send.js';
|
|
15
|
+
|
|
16
|
+
type MaxChannelPlugin = ChannelPlugin<ResolvedMaxAccount>;
|
|
17
|
+
|
|
18
|
+
// ── MAX group policy helpers ──
|
|
19
|
+
// These mirror resolveChannelGroupRequireMention/resolveChannelGroupToolsPolicy
|
|
20
|
+
// (internal SDK functions not exported) but for the "max" channel.
|
|
21
|
+
|
|
22
|
+
function resolveMaxGroupConfig(
|
|
23
|
+
cfg: OpenClawConfig,
|
|
24
|
+
groupId?: string | null,
|
|
25
|
+
accountId?: string | null,
|
|
26
|
+
) {
|
|
27
|
+
const maxSection = (cfg.channels as Record<string, unknown>)?.max as
|
|
28
|
+
Record<string, unknown> | undefined;
|
|
29
|
+
if (!maxSection) return { groupConfig: undefined, defaultConfig: undefined };
|
|
30
|
+
|
|
31
|
+
// Resolve groups map: account-level takes priority over channel-level
|
|
32
|
+
let groups: Record<string, unknown> | undefined;
|
|
33
|
+
if (accountId && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
34
|
+
const accounts = maxSection.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
35
|
+
groups = accounts?.[accountId]?.groups as Record<string, unknown> | undefined;
|
|
36
|
+
}
|
|
37
|
+
if (!groups) {
|
|
38
|
+
groups = maxSection.groups as Record<string, unknown> | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalizedId = groupId?.trim();
|
|
42
|
+
const groupConfig =
|
|
43
|
+
normalizedId && groups
|
|
44
|
+
? (groups[normalizedId] as Record<string, unknown> | undefined)
|
|
45
|
+
: undefined;
|
|
46
|
+
const defaultConfig = groups?.['*'] as Record<string, unknown> | undefined;
|
|
47
|
+
|
|
48
|
+
return { groupConfig, defaultConfig };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveMaxGroupRequireMention(params: {
|
|
52
|
+
cfg: OpenClawConfig;
|
|
53
|
+
groupId?: string | null;
|
|
54
|
+
accountId?: string | null;
|
|
55
|
+
}): boolean {
|
|
56
|
+
const { groupConfig, defaultConfig } = resolveMaxGroupConfig(
|
|
57
|
+
params.cfg,
|
|
58
|
+
params.groupId,
|
|
59
|
+
params.accountId,
|
|
60
|
+
);
|
|
61
|
+
const configMention =
|
|
62
|
+
typeof groupConfig?.requireMention === 'boolean'
|
|
63
|
+
? groupConfig.requireMention
|
|
64
|
+
: typeof defaultConfig?.requireMention === 'boolean'
|
|
65
|
+
? defaultConfig.requireMention
|
|
66
|
+
: undefined;
|
|
67
|
+
if (typeof configMention === 'boolean') return configMention;
|
|
68
|
+
return true; // default: require mention
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveMaxGroupToolPolicy(params: {
|
|
72
|
+
cfg: OpenClawConfig;
|
|
73
|
+
groupId?: string | null;
|
|
74
|
+
accountId?: string | null;
|
|
75
|
+
senderId?: string | null;
|
|
76
|
+
senderName?: string | null;
|
|
77
|
+
senderUsername?: string | null;
|
|
78
|
+
senderE164?: string | null;
|
|
79
|
+
}): GroupToolPolicyConfig | undefined {
|
|
80
|
+
const { groupConfig, defaultConfig } = resolveMaxGroupConfig(
|
|
81
|
+
params.cfg,
|
|
82
|
+
params.groupId,
|
|
83
|
+
params.accountId,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Group-level sender-specific policy
|
|
87
|
+
const groupSenderPolicy = resolveToolsBySender({
|
|
88
|
+
toolsBySender: groupConfig?.toolsBySender as Record<string, GroupToolPolicyConfig> | undefined,
|
|
89
|
+
senderId: params.senderId,
|
|
90
|
+
senderName: params.senderName,
|
|
91
|
+
senderUsername: params.senderUsername,
|
|
92
|
+
senderE164: params.senderE164,
|
|
93
|
+
});
|
|
94
|
+
if (groupSenderPolicy) return groupSenderPolicy;
|
|
95
|
+
if (groupConfig?.tools) return groupConfig.tools as GroupToolPolicyConfig;
|
|
96
|
+
|
|
97
|
+
// Default config fallback
|
|
98
|
+
const defaultSenderPolicy = resolveToolsBySender({
|
|
99
|
+
toolsBySender: defaultConfig?.toolsBySender as
|
|
100
|
+
Record<string, GroupToolPolicyConfig> | undefined,
|
|
101
|
+
senderId: params.senderId,
|
|
102
|
+
senderName: params.senderName,
|
|
103
|
+
senderUsername: params.senderUsername,
|
|
104
|
+
senderE164: params.senderE164,
|
|
105
|
+
});
|
|
106
|
+
if (defaultSenderPolicy) return defaultSenderPolicy;
|
|
107
|
+
if (defaultConfig?.tools) return defaultConfig.tools as GroupToolPolicyConfig;
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** DM policy (default pairing) and warnings for open group policy. */
|
|
113
|
+
export const maxSecurityAdapter: NonNullable<MaxChannelPlugin['security']> = {
|
|
114
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
115
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
116
|
+
const maxSection = (cfg.channels as Record<string, unknown>)?.max as
|
|
117
|
+
Record<string, unknown> | undefined;
|
|
118
|
+
const useAccountPath = Boolean(
|
|
119
|
+
(maxSection?.accounts as Record<string, unknown>)?.[resolvedAccountId],
|
|
120
|
+
);
|
|
121
|
+
const basePath = useAccountPath
|
|
122
|
+
? `channels.max.accounts.${resolvedAccountId}.`
|
|
123
|
+
: 'channels.max.';
|
|
124
|
+
return {
|
|
125
|
+
policy: account.config.dmPolicy ?? 'pairing',
|
|
126
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
127
|
+
policyPath: `${basePath}dmPolicy`,
|
|
128
|
+
allowFromPath: basePath,
|
|
129
|
+
approveHint: formatPairingApproveHint('max'),
|
|
130
|
+
normalizeEntry: (raw: string) => raw.replace(/^max:/i, ''),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
collectWarnings: ({ account, cfg }) => {
|
|
134
|
+
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
135
|
+
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? 'allowlist';
|
|
136
|
+
if (groupPolicy !== 'open') {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const groupAllowlistConfigured =
|
|
140
|
+
account.config.groups && Object.keys(account.config.groups).length > 0;
|
|
141
|
+
if (groupAllowlistConfigured) {
|
|
142
|
+
return [
|
|
143
|
+
`- MAX groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.max.groupPolicy="allowlist" + channels.max.groupAllowFrom to restrict senders.`,
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
return [
|
|
147
|
+
`- MAX groups: groupPolicy="open" with no channels.max.groups allowlist; any group can add + ping (mention-gated). Set channels.max.groupPolicy="allowlist" + channels.max.groupAllowFrom or configure channels.max.groups.`,
|
|
148
|
+
];
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Group requireMention (default true) and tool policy from channels.max.groups. */
|
|
153
|
+
export const maxGroupsAdapter: NonNullable<MaxChannelPlugin['groups']> = {
|
|
154
|
+
resolveRequireMention: ({ cfg, groupId, accountId }) =>
|
|
155
|
+
resolveMaxGroupRequireMention({ cfg, groupId, accountId }),
|
|
156
|
+
resolveToolPolicy: ({
|
|
157
|
+
cfg,
|
|
158
|
+
groupId,
|
|
159
|
+
accountId,
|
|
160
|
+
senderId,
|
|
161
|
+
senderName,
|
|
162
|
+
senderUsername,
|
|
163
|
+
senderE164,
|
|
164
|
+
}) =>
|
|
165
|
+
resolveMaxGroupToolPolicy({
|
|
166
|
+
cfg,
|
|
167
|
+
groupId,
|
|
168
|
+
accountId,
|
|
169
|
+
senderId,
|
|
170
|
+
senderName,
|
|
171
|
+
senderUsername,
|
|
172
|
+
senderE164,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Pairing ids are MAX user ids; approval is announced to the user in MAX. */
|
|
177
|
+
export const maxPairingAdapter: NonNullable<MaxChannelPlugin['pairing']> = {
|
|
178
|
+
idLabel: 'maxUserId',
|
|
179
|
+
normalizeAllowEntry: (entry) => entry.replace(/^max:/i, ''),
|
|
180
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
181
|
+
const account = resolveMaxAccount({ cfg });
|
|
182
|
+
if (!account.token) throw new Error('MAX bot token not configured');
|
|
183
|
+
await sendMaxMessage(id, PAIRING_APPROVED_MESSAGE, { token: account.token });
|
|
184
|
+
},
|
|
185
|
+
};
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX channel plugin for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Implements the ChannelPlugin interface to integrate MAX messenger. The
|
|
5
|
+
* adapters live next to this file by role: channel-config (accounts, setup),
|
|
6
|
+
* channel-policy (DM/group access, pairing), channel-directory (targets,
|
|
7
|
+
* directory), channel-outbound (delivery), channel-lifecycle (start/stop,
|
|
8
|
+
* status, health), channel-agent-prompt; message tool actions in actions.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ChannelMeta } from 'openclaw/plugin-sdk/channel-contract';
|
|
12
|
+
import type { ChannelPlugin } from 'openclaw/plugin-sdk/channel-core';
|
|
13
|
+
import { buildChannelConfigSchema } from 'openclaw/plugin-sdk/core';
|
|
14
|
+
|
|
15
|
+
import type { ResolvedMaxAccount } from './accounts.js';
|
|
16
|
+
import { maxMessageActions } from './actions.js';
|
|
17
|
+
import { maxAgentPromptAdapter } from './channel-agent-prompt.js';
|
|
18
|
+
import { maxConfigAdapter, maxSetupAdapter } from './channel-config.js';
|
|
19
|
+
import { maxDirectoryAdapter, maxMessagingAdapter } from './channel-directory.js';
|
|
20
|
+
import { maxGatewayAdapter, maxStatusAdapter } from './channel-lifecycle.js';
|
|
21
|
+
import { maxOutboundAdapter } from './channel-outbound.js';
|
|
22
|
+
import { maxGroupsAdapter, maxPairingAdapter, maxSecurityAdapter } from './channel-policy.js';
|
|
23
|
+
import { MaxConfigSchema } from './config-schema.js';
|
|
24
|
+
import {
|
|
25
|
+
buildMaxModelBrowseChannelData,
|
|
26
|
+
buildMaxModelsAddProviderChannelData,
|
|
27
|
+
buildMaxModelsListChannelData,
|
|
28
|
+
buildMaxModelsMenuChannelData,
|
|
29
|
+
buildMaxModelsProviderChannelData,
|
|
30
|
+
} from './model-buttons.js';
|
|
31
|
+
import { maxSetupWizard } from './onboarding.js';
|
|
32
|
+
|
|
33
|
+
// ── Meta ──
|
|
34
|
+
|
|
35
|
+
const maxMeta: ChannelMeta = {
|
|
36
|
+
id: 'max',
|
|
37
|
+
label: 'MAX',
|
|
38
|
+
selectionLabel: 'MAX Messenger',
|
|
39
|
+
docsPath: '/channels/max',
|
|
40
|
+
blurb: 'MAX messenger bot via platform-api2.max.ru. Supports DMs, groups, inline keyboards.',
|
|
41
|
+
order: 50,
|
|
42
|
+
aliases: ['max-messenger'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── Channel Plugin ──
|
|
46
|
+
|
|
47
|
+
export const maxPlugin: ChannelPlugin<ResolvedMaxAccount> = {
|
|
48
|
+
id: 'max',
|
|
49
|
+
meta: maxMeta,
|
|
50
|
+
setupWizard: maxSetupWizard,
|
|
51
|
+
configSchema: buildChannelConfigSchema(
|
|
52
|
+
MaxConfigSchema as unknown as Parameters<typeof buildChannelConfigSchema>[0],
|
|
53
|
+
),
|
|
54
|
+
|
|
55
|
+
capabilities: {
|
|
56
|
+
chatTypes: ['direct', 'group', 'channel'],
|
|
57
|
+
reactions: false,
|
|
58
|
+
threads: false,
|
|
59
|
+
media: true,
|
|
60
|
+
nativeCommands: true,
|
|
61
|
+
blockStreaming: false,
|
|
62
|
+
edit: true,
|
|
63
|
+
// DELETE /messages backs the "delete" action; replies use link.type=reply.
|
|
64
|
+
unsend: true,
|
|
65
|
+
reply: true,
|
|
66
|
+
polls: false,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
reload: { configPrefixes: ['channels.max'] },
|
|
70
|
+
|
|
71
|
+
commands: {
|
|
72
|
+
nativeCommandsAutoEnabled: true,
|
|
73
|
+
nativeSkillsAutoEnabled: true,
|
|
74
|
+
buildModelsMenuChannelData: buildMaxModelsMenuChannelData,
|
|
75
|
+
buildModelsProviderChannelData: buildMaxModelsProviderChannelData,
|
|
76
|
+
buildModelsAddProviderChannelData: buildMaxModelsAddProviderChannelData,
|
|
77
|
+
buildModelsListChannelData: buildMaxModelsListChannelData,
|
|
78
|
+
buildModelBrowseChannelData: buildMaxModelBrowseChannelData,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
agentPrompt: maxAgentPromptAdapter,
|
|
82
|
+
config: maxConfigAdapter,
|
|
83
|
+
security: maxSecurityAdapter,
|
|
84
|
+
groups: maxGroupsAdapter,
|
|
85
|
+
pairing: maxPairingAdapter,
|
|
86
|
+
|
|
87
|
+
threading: {
|
|
88
|
+
resolveReplyToMode: () => 'first',
|
|
89
|
+
},
|
|
90
|
+
messaging: maxMessagingAdapter,
|
|
91
|
+
directory: maxDirectoryAdapter,
|
|
92
|
+
outbound: maxOutboundAdapter,
|
|
93
|
+
setup: maxSetupAdapter,
|
|
94
|
+
status: maxStatusAdapter,
|
|
95
|
+
gateway: maxGatewayAdapter,
|
|
96
|
+
|
|
97
|
+
// Message actions (send, edit, delete)
|
|
98
|
+
actions: maxMessageActions,
|
|
99
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX channel config Zod schema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MarkdownConfigSchema, ToolPolicySchema } from 'openclaw/plugin-sdk/channel-config-schema';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
// Defaulted policy fields are built with this package's own zod: wrapping the
|
|
9
|
+
// SDK's enum instances in `.optional().default()` breaks as soon as the plugin
|
|
10
|
+
// and the gateway resolve different zod copies (4.4 vs 4.6 → "expected
|
|
11
|
+
// nonoptional"). Values mirror the SDK's DmPolicySchema/GroupPolicySchema.
|
|
12
|
+
export const DmPolicySchema = z.enum(['pairing', 'allowlist', 'open', 'disabled']);
|
|
13
|
+
export const GroupPolicySchema = z.enum(['open', 'disabled', 'allowlist']);
|
|
14
|
+
|
|
15
|
+
// These schemas were removed from the public plugin-sdk surface in OpenClaw 2026.3.x.
|
|
16
|
+
// Inlined here to stay compatible with both old and new runtimes.
|
|
17
|
+
const BlockStreamingCoalesceSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
minChars: z.number().int().optional(),
|
|
20
|
+
maxChars: z.number().int().optional(),
|
|
21
|
+
idleMs: z.number().int().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
const DmConfigSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
|
|
31
|
+
function requireOpenAllowFrom(params: {
|
|
32
|
+
policy: string | undefined;
|
|
33
|
+
allowFrom: unknown[] | undefined;
|
|
34
|
+
ctx: z.RefinementCtx;
|
|
35
|
+
path: (string | number)[];
|
|
36
|
+
message: string;
|
|
37
|
+
}): void {
|
|
38
|
+
if (params.policy !== 'open') return;
|
|
39
|
+
const normalized = (params.allowFrom ?? []).map(String);
|
|
40
|
+
if (normalized.includes('*')) return;
|
|
41
|
+
params.ctx.addIssue({
|
|
42
|
+
code: z.ZodIssueCode.custom,
|
|
43
|
+
path: params.path,
|
|
44
|
+
message: params.message,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-group config for MAX chats
|
|
50
|
+
*/
|
|
51
|
+
export const MaxGroupSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
requireMention: z.boolean().optional(),
|
|
54
|
+
tools: ToolPolicySchema.optional(),
|
|
55
|
+
skills: z.array(z.string()).optional(),
|
|
56
|
+
enabled: z.boolean().optional(),
|
|
57
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
58
|
+
systemPrompt: z.string().optional(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* MAX account config (base schema for both top-level and accounts.*)
|
|
64
|
+
*/
|
|
65
|
+
export const MaxAccountSchemaBase = z
|
|
66
|
+
.object({
|
|
67
|
+
name: z.string().optional(),
|
|
68
|
+
enabled: z.boolean().optional(),
|
|
69
|
+
markdown: MarkdownConfigSchema.optional(),
|
|
70
|
+
botToken: z.string().optional(),
|
|
71
|
+
tokenFile: z.string().optional(),
|
|
72
|
+
dmPolicy: DmPolicySchema.optional().default('pairing'),
|
|
73
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
74
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
75
|
+
groupPolicy: GroupPolicySchema.optional().default('allowlist'),
|
|
76
|
+
groups: z.record(z.string(), MaxGroupSchema.optional()).optional(),
|
|
77
|
+
/** Update transport; default: "webhook" when webhookUrl is set, otherwise "polling" */
|
|
78
|
+
transport: z.enum(['polling', 'webhook']).optional(),
|
|
79
|
+
webhookUrl: z.string().optional(),
|
|
80
|
+
/** MAX secret: 5–256 of [A-Za-z0-9_-]; checked at start */
|
|
81
|
+
webhookSecret: z.string().optional(),
|
|
82
|
+
/** File holding the webhook secret (regular file, not a symlink) */
|
|
83
|
+
webhookSecretFile: z.string().optional(),
|
|
84
|
+
webhookPath: z.string().optional(),
|
|
85
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
86
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
87
|
+
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
|
88
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
89
|
+
blockStreaming: z.boolean().optional(),
|
|
90
|
+
streamMode: z.enum(['off', 'partial', 'block']).optional(),
|
|
91
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
92
|
+
responsePrefix: z.string().optional(),
|
|
93
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
94
|
+
markSeen: z.boolean().optional(),
|
|
95
|
+
actions: z
|
|
96
|
+
.record(
|
|
97
|
+
z.string(),
|
|
98
|
+
z.union([z.boolean(), z.enum(['pairing', 'allowlist', 'open'])]).optional(),
|
|
99
|
+
)
|
|
100
|
+
.optional(),
|
|
101
|
+
})
|
|
102
|
+
.strict();
|
|
103
|
+
|
|
104
|
+
function requireWebhookUrl(params: {
|
|
105
|
+
transport: string | undefined;
|
|
106
|
+
webhookUrl: string | undefined;
|
|
107
|
+
ctx: z.RefinementCtx;
|
|
108
|
+
}): void {
|
|
109
|
+
if (params.transport !== 'webhook' || params.webhookUrl?.trim()) return;
|
|
110
|
+
params.ctx.addIssue({
|
|
111
|
+
code: z.ZodIssueCode.custom,
|
|
112
|
+
path: ['webhookUrl'],
|
|
113
|
+
message: 'channels.max.transport="webhook" requires channels.max.webhookUrl',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Individual account schema (with open-policy validation)
|
|
119
|
+
*/
|
|
120
|
+
export const MaxAccountSchema = MaxAccountSchemaBase.superRefine((value, ctx) => {
|
|
121
|
+
requireOpenAllowFrom({
|
|
122
|
+
policy: value.dmPolicy,
|
|
123
|
+
allowFrom: value.allowFrom,
|
|
124
|
+
ctx,
|
|
125
|
+
path: ['allowFrom'],
|
|
126
|
+
message: 'channels.max.dmPolicy="open" requires channels.max.allowFrom to include "*"',
|
|
127
|
+
});
|
|
128
|
+
requireWebhookUrl({ transport: value.transport, webhookUrl: value.webhookUrl, ctx });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** Bot command registered via PATCH /me/commands (name ≤64 chars without slash, description ≤128) */
|
|
132
|
+
const MaxBotCommandSchema = z
|
|
133
|
+
.object({
|
|
134
|
+
name: z.string().min(1).max(64),
|
|
135
|
+
description: z.string().max(128).optional(),
|
|
136
|
+
})
|
|
137
|
+
.strict();
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Top-level MAX config schema (supports accounts.* sub-configs)
|
|
141
|
+
*/
|
|
142
|
+
export const MaxConfigSchema = MaxAccountSchemaBase.extend({
|
|
143
|
+
accounts: z.record(z.string(), MaxAccountSchema.optional()).optional(),
|
|
144
|
+
commands: z.array(MaxBotCommandSchema).max(32).optional(),
|
|
145
|
+
}).superRefine((value, ctx) => {
|
|
146
|
+
requireOpenAllowFrom({
|
|
147
|
+
policy: value.dmPolicy,
|
|
148
|
+
allowFrom: value.allowFrom,
|
|
149
|
+
ctx,
|
|
150
|
+
path: ['allowFrom'],
|
|
151
|
+
message: 'channels.max.dmPolicy="open" requires channels.max.allowFrom to include "*"',
|
|
152
|
+
});
|
|
153
|
+
requireWebhookUrl({ transport: value.transport, webhookUrl: value.webhookUrl, ctx });
|
|
154
|
+
});
|