@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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX webhook transport — HTTP ingress on the gateway server and the
|
|
3
|
+
* subscription lifecycle (GET/POST/DELETE /subscriptions).
|
|
4
|
+
*
|
|
5
|
+
* The route is registered dynamically from the account lifecycle through
|
|
6
|
+
* `registerPluginHttpRoute` (plugin-sdk/webhook-ingress): it lives exactly as
|
|
7
|
+
* long as the account task, carries plugin-managed auth (the MAX shared
|
|
8
|
+
* secret), and a failed registration fails the account start instead of
|
|
9
|
+
* reporting ready without live ingress.
|
|
10
|
+
*/
|
|
11
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { normalizeWebhookPath, readJsonWebhookBodyOrReject, registerPluginHttpRoute, resolveWebhookPath, } from 'openclaw/plugin-sdk/webhook-ingress';
|
|
13
|
+
/** Plugin id from openclaw.plugin.json — the owner of the gateway route. */
|
|
14
|
+
export const MAX_PLUGIN_ID = 'openclaw-max';
|
|
15
|
+
/** Stable same-plugin route sub-owner (see gateway-routes docs). */
|
|
16
|
+
export const MAX_WEBHOOK_ROUTE_SOURCE = 'max-webhook';
|
|
17
|
+
export const DEFAULT_MAX_WEBHOOK_PATH = '/max/webhook';
|
|
18
|
+
/** Header MAX sends with the subscription secret. */
|
|
19
|
+
const SECRET_HEADER = 'x-max-bot-api-secret';
|
|
20
|
+
const DEDUPE_CAPACITY = 1000;
|
|
21
|
+
function secretsMatch(expected, provided) {
|
|
22
|
+
const a = Buffer.from(expected);
|
|
23
|
+
const b = Buffer.from(provided);
|
|
24
|
+
if (a.length !== b.length)
|
|
25
|
+
return false;
|
|
26
|
+
return timingSafeEqual(a, b);
|
|
27
|
+
}
|
|
28
|
+
const webhookTargets = new Map();
|
|
29
|
+
export function resolveMaxWebhookPath(webhookPath, webhookUrl) {
|
|
30
|
+
return (resolveWebhookPath({ webhookPath, webhookUrl, defaultPath: DEFAULT_MAX_WEBHOOK_PATH }) ??
|
|
31
|
+
DEFAULT_MAX_WEBHOOK_PATH);
|
|
32
|
+
}
|
|
33
|
+
export function registerMaxWebhookTarget(target) {
|
|
34
|
+
const key = normalizeWebhookPath(target.path);
|
|
35
|
+
const normalizedTarget = { ...target, path: key, seen: new Map() };
|
|
36
|
+
webhookTargets.set(key, [...(webhookTargets.get(key) ?? []), normalizedTarget]);
|
|
37
|
+
let active = true;
|
|
38
|
+
return () => {
|
|
39
|
+
if (!active)
|
|
40
|
+
return;
|
|
41
|
+
active = false;
|
|
42
|
+
const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
|
|
43
|
+
if (updated.length > 0) {
|
|
44
|
+
webhookTargets.set(key, updated);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
webhookTargets.delete(key);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Register the gateway HTTP route for a webhook path. Canonically equal paths
|
|
53
|
+
* of several accounts share one route: the handler picks the target by secret.
|
|
54
|
+
*/
|
|
55
|
+
export function registerMaxWebhookRoute(params) {
|
|
56
|
+
const register = params.register ?? registerPluginHttpRoute;
|
|
57
|
+
return register({
|
|
58
|
+
path: normalizeWebhookPath(params.path),
|
|
59
|
+
auth: 'plugin',
|
|
60
|
+
match: 'exact',
|
|
61
|
+
pluginId: MAX_PLUGIN_ID,
|
|
62
|
+
source: MAX_WEBHOOK_ROUTE_SOURCE,
|
|
63
|
+
accountId: params.accountId,
|
|
64
|
+
replaceExisting: true,
|
|
65
|
+
throwOnFailure: true,
|
|
66
|
+
log: params.log,
|
|
67
|
+
handler: handleMaxWebhookRequest,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Key for redelivery dedupe: MAX retries a delivery that was not acknowledged
|
|
72
|
+
* with 200 in time, so the same update can arrive twice.
|
|
73
|
+
* @internal exported for testing.
|
|
74
|
+
*/
|
|
75
|
+
export function maxUpdateDedupeKey(update) {
|
|
76
|
+
const id = update.callback?.callback_id ??
|
|
77
|
+
update.message?.body?.mid ??
|
|
78
|
+
(typeof update.message_id === 'string' ? update.message_id : undefined);
|
|
79
|
+
if (!id && update.timestamp == null)
|
|
80
|
+
return undefined;
|
|
81
|
+
return `${update.update_type}:${update.timestamp ?? ''}:${id ?? ''}`;
|
|
82
|
+
}
|
|
83
|
+
/** Returns true when the key was already seen (LRU bounded by DEDUPE_CAPACITY). */
|
|
84
|
+
function rememberUpdate(seen, key) {
|
|
85
|
+
if (seen.has(key)) {
|
|
86
|
+
seen.delete(key);
|
|
87
|
+
seen.set(key, true);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
seen.set(key, true);
|
|
91
|
+
if (seen.size > DEDUPE_CAPACITY) {
|
|
92
|
+
const oldest = seen.keys().next().value;
|
|
93
|
+
if (oldest !== undefined)
|
|
94
|
+
seen.delete(oldest);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
export async function handleMaxWebhookRequest(req, res) {
|
|
99
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
100
|
+
const path = normalizeWebhookPath(url.pathname);
|
|
101
|
+
const targets = webhookTargets.get(path);
|
|
102
|
+
if (!targets || targets.length === 0) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (req.method !== 'POST') {
|
|
106
|
+
res.statusCode = 405;
|
|
107
|
+
res.setHeader('Allow', 'POST');
|
|
108
|
+
res.end('Method Not Allowed');
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Authenticate before reading the body. Targets without a secret never
|
|
112
|
+
// match: an unauthenticated webhook would let anyone inject updates.
|
|
113
|
+
const rawSecret = req.headers[SECRET_HEADER];
|
|
114
|
+
const providedSecret = Array.isArray(rawSecret) ? (rawSecret[0] ?? '') : (rawSecret ?? '');
|
|
115
|
+
const matchedTarget = providedSecret
|
|
116
|
+
? targets.find((target) => target.secret && secretsMatch(target.secret, providedSecret))
|
|
117
|
+
: undefined;
|
|
118
|
+
if (!matchedTarget) {
|
|
119
|
+
res.statusCode = 401;
|
|
120
|
+
res.end('Unauthorized');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Size/timeout/closed-connection/malformed-JSON failures are answered by the
|
|
124
|
+
// SDK helper (413/408/400) — it has already written the response.
|
|
125
|
+
const body = await readJsonWebhookBodyOrReject({
|
|
126
|
+
req,
|
|
127
|
+
res,
|
|
128
|
+
maxBytes: 1024 * 1024,
|
|
129
|
+
timeoutMs: 30_000,
|
|
130
|
+
emptyObjectOnEmpty: false,
|
|
131
|
+
invalidJsonMessage: 'invalid payload',
|
|
132
|
+
});
|
|
133
|
+
if (!body.ok) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const raw = body.value;
|
|
137
|
+
if (!raw ||
|
|
138
|
+
typeof raw !== 'object' ||
|
|
139
|
+
Array.isArray(raw) ||
|
|
140
|
+
typeof raw.update_type !== 'string') {
|
|
141
|
+
res.statusCode = 400;
|
|
142
|
+
res.end('invalid payload');
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
const update = raw;
|
|
146
|
+
// MAX needs 200 within 30 s while an agent turn can take minutes: ack first,
|
|
147
|
+
// then hand the update to the account task. Errors are logged, never turned
|
|
148
|
+
// into a non-200 (that would only make MAX redeliver and eventually
|
|
149
|
+
// unsubscribe the bot). onUpdate must only enqueue: this request's work
|
|
150
|
+
// admission is released when the handler returns, so agent/session work
|
|
151
|
+
// started from here would fail with GatewayDrainingError.
|
|
152
|
+
res.statusCode = 200;
|
|
153
|
+
res.setHeader('Content-Type', 'application/json');
|
|
154
|
+
res.end(JSON.stringify({ ok: true }));
|
|
155
|
+
const dedupeKey = maxUpdateDedupeKey(update);
|
|
156
|
+
if (dedupeKey && rememberUpdate(matchedTarget.seen, dedupeKey)) {
|
|
157
|
+
matchedTarget.log?.(`[${matchedTarget.account.accountId}] MAX webhook: duplicate ${dedupeKey} skipped`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
void Promise.resolve()
|
|
161
|
+
.then(() => matchedTarget.onUpdate(update))
|
|
162
|
+
.catch((err) => {
|
|
163
|
+
matchedTarget.error?.(`[${matchedTarget.account.accountId}] Webhook update processing failed: ${String(err)}`);
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Subscribe to MAX webhook
|
|
169
|
+
*/
|
|
170
|
+
export async function subscribeMaxWebhook(params) {
|
|
171
|
+
const { api, webhookUrl, secret, updateTypes } = params;
|
|
172
|
+
await api.subscribe({
|
|
173
|
+
url: webhookUrl,
|
|
174
|
+
update_types: updateTypes ?? [
|
|
175
|
+
'message_created',
|
|
176
|
+
'message_callback',
|
|
177
|
+
'message_edited',
|
|
178
|
+
'message_removed',
|
|
179
|
+
'bot_started',
|
|
180
|
+
'bot_stopped',
|
|
181
|
+
'bot_added',
|
|
182
|
+
'bot_removed',
|
|
183
|
+
'dialog_cleared',
|
|
184
|
+
'dialog_removed',
|
|
185
|
+
'chat_title_changed',
|
|
186
|
+
],
|
|
187
|
+
secret,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Unsubscribe from MAX webhook
|
|
192
|
+
*/
|
|
193
|
+
export async function unsubscribeMaxWebhook(params) {
|
|
194
|
+
const { api, webhookUrl } = params;
|
|
195
|
+
await api.unsubscribe(webhookUrl);
|
|
196
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-max",
|
|
3
|
+
"name": "MAX",
|
|
4
|
+
"description": "MAX messenger channel plugin (max.ru Bot API via platform-api2.max.ru, long polling or webhook)",
|
|
5
|
+
"version": "0.7.2",
|
|
6
|
+
"channels": [
|
|
7
|
+
"max"
|
|
8
|
+
],
|
|
9
|
+
"channelEnvVars": {
|
|
10
|
+
"max": [
|
|
11
|
+
"MAX_BOT_TOKEN"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"channelConfigs": {
|
|
15
|
+
"max": {
|
|
16
|
+
"schema": {
|
|
17
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
18
|
+
"type": "object",
|
|
19
|
+
"definitions": {
|
|
20
|
+
"allowList": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"anyOf": [
|
|
24
|
+
{
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "number"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"groupConfig": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"requireMention": {
|
|
37
|
+
"type": "boolean"
|
|
38
|
+
},
|
|
39
|
+
"tools": {
|
|
40
|
+
"type": "object"
|
|
41
|
+
},
|
|
42
|
+
"skills": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"enabled": {
|
|
49
|
+
"type": "boolean"
|
|
50
|
+
},
|
|
51
|
+
"allowFrom": {
|
|
52
|
+
"$ref": "#/definitions/allowList"
|
|
53
|
+
},
|
|
54
|
+
"systemPrompt": {
|
|
55
|
+
"type": "string"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
},
|
|
60
|
+
"accountConfig": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"name": {
|
|
64
|
+
"type": "string"
|
|
65
|
+
},
|
|
66
|
+
"enabled": {
|
|
67
|
+
"type": "boolean"
|
|
68
|
+
},
|
|
69
|
+
"markdown": {
|
|
70
|
+
"type": "object"
|
|
71
|
+
},
|
|
72
|
+
"botToken": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"tokenFile": {
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"dmPolicy": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"enum": [
|
|
81
|
+
"pairing",
|
|
82
|
+
"allowlist",
|
|
83
|
+
"open",
|
|
84
|
+
"disabled"
|
|
85
|
+
],
|
|
86
|
+
"default": "pairing"
|
|
87
|
+
},
|
|
88
|
+
"allowFrom": {
|
|
89
|
+
"$ref": "#/definitions/allowList"
|
|
90
|
+
},
|
|
91
|
+
"groupAllowFrom": {
|
|
92
|
+
"$ref": "#/definitions/allowList"
|
|
93
|
+
},
|
|
94
|
+
"groupPolicy": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": [
|
|
97
|
+
"allowlist",
|
|
98
|
+
"open",
|
|
99
|
+
"disabled"
|
|
100
|
+
],
|
|
101
|
+
"default": "allowlist"
|
|
102
|
+
},
|
|
103
|
+
"groups": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"additionalProperties": {
|
|
106
|
+
"$ref": "#/definitions/groupConfig"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"transport": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"enum": [
|
|
112
|
+
"polling",
|
|
113
|
+
"webhook"
|
|
114
|
+
],
|
|
115
|
+
"description": "Update transport. Default: webhook when webhookUrl is set, otherwise polling. MAX does not serve long polling while a webhook subscription exists."
|
|
116
|
+
},
|
|
117
|
+
"webhookUrl": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "Public HTTPS URL (port 443, trusted CA) MAX posts updates to; its path is served on the gateway HTTP server."
|
|
120
|
+
},
|
|
121
|
+
"webhookSecret": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "X-Max-Bot-Api-Secret value, 5-256 of A-Z a-z 0-9 _ -. Prefer webhookSecretFile; generated and kept in the account state file when neither is set."
|
|
124
|
+
},
|
|
125
|
+
"webhookSecretFile": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "File holding the webhook secret (like tokenFile)."
|
|
128
|
+
},
|
|
129
|
+
"webhookPath": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Gateway route path when it differs from the webhookUrl pathname (default: webhookUrl pathname, else /max/webhook)."
|
|
132
|
+
},
|
|
133
|
+
"historyLimit": {
|
|
134
|
+
"type": "integer",
|
|
135
|
+
"minimum": 0
|
|
136
|
+
},
|
|
137
|
+
"dmHistoryLimit": {
|
|
138
|
+
"type": "integer",
|
|
139
|
+
"minimum": 0
|
|
140
|
+
},
|
|
141
|
+
"dms": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"historyLimit": {
|
|
147
|
+
"type": "integer",
|
|
148
|
+
"minimum": 0
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"additionalProperties": false
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"textChunkLimit": {
|
|
155
|
+
"type": "integer",
|
|
156
|
+
"minimum": 1
|
|
157
|
+
},
|
|
158
|
+
"blockStreaming": {
|
|
159
|
+
"type": "boolean"
|
|
160
|
+
},
|
|
161
|
+
"streamMode": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"enum": [
|
|
164
|
+
"off",
|
|
165
|
+
"partial",
|
|
166
|
+
"block"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
"blockStreamingCoalesce": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"minChars": {
|
|
173
|
+
"type": "integer"
|
|
174
|
+
},
|
|
175
|
+
"maxChars": {
|
|
176
|
+
"type": "integer"
|
|
177
|
+
},
|
|
178
|
+
"idleMs": {
|
|
179
|
+
"type": "integer"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"additionalProperties": false
|
|
183
|
+
},
|
|
184
|
+
"responsePrefix": {
|
|
185
|
+
"type": "string"
|
|
186
|
+
},
|
|
187
|
+
"mediaMaxMb": {
|
|
188
|
+
"type": "number",
|
|
189
|
+
"exclusiveMinimum": 0
|
|
190
|
+
},
|
|
191
|
+
"markSeen": {
|
|
192
|
+
"type": "boolean"
|
|
193
|
+
},
|
|
194
|
+
"actions": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"additionalProperties": {
|
|
197
|
+
"anyOf": [
|
|
198
|
+
{
|
|
199
|
+
"type": "boolean"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"type": "string",
|
|
203
|
+
"enum": [
|
|
204
|
+
"pairing",
|
|
205
|
+
"allowlist",
|
|
206
|
+
"open"
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"allOf": [
|
|
216
|
+
{
|
|
217
|
+
"$ref": "#/definitions/accountConfig"
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"properties": {
|
|
221
|
+
"accounts": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"additionalProperties": {
|
|
224
|
+
"$ref": "#/definitions/accountConfig"
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"commands": {
|
|
228
|
+
"type": "array",
|
|
229
|
+
"description": "Bot commands registered on start via PATCH /me/commands (replaces the whole list, max 32).",
|
|
230
|
+
"items": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": {
|
|
233
|
+
"name": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"maxLength": 64
|
|
236
|
+
},
|
|
237
|
+
"description": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"maxLength": 128
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"required": [
|
|
243
|
+
"name"
|
|
244
|
+
],
|
|
245
|
+
"additionalProperties": false
|
|
246
|
+
},
|
|
247
|
+
"maxItems": 32
|
|
248
|
+
},
|
|
249
|
+
"defaultAccount": {
|
|
250
|
+
"type": "string"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"label": "MAX",
|
|
255
|
+
"description": "MAX messenger channel (max.ru Bot API)."
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"configSchema": {
|
|
259
|
+
"type": "object",
|
|
260
|
+
"additionalProperties": false,
|
|
261
|
+
"properties": {}
|
|
262
|
+
}
|
|
263
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aspalagin/openclaw-max",
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "MAX messenger channel plugin for OpenClaw",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"openclaw-plugin",
|
|
8
|
+
"max",
|
|
9
|
+
"max-messenger",
|
|
10
|
+
"messenger",
|
|
11
|
+
"ai-agent"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/aspalagin/openclaw-max.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/aspalagin/openclaw-max#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/aspalagin/openclaw-max/issues"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist-ts dist/src && tsc --noEmit false --noEmitOnError false --outDir dist-ts && mkdir -p dist/src && cp dist-ts/index.js dist-ts/setup-entry.js dist/ && cp dist-ts/src/*.js dist/src/ && rm -f dist/src/*.test.js dist/src/test-setup.js",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint src/",
|
|
32
|
+
"lint:fix": "eslint src/ --fix",
|
|
33
|
+
"format": "prettier --write \"src/**/*.ts\" \"*.ts\"",
|
|
34
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"*.ts\"",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:api": "node --input-type=module ./scripts/test-api.mjs",
|
|
38
|
+
"check:cycles": "node scripts/check-cycles.mjs",
|
|
39
|
+
"schema:update": "node scripts/update-schema.mjs"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist/",
|
|
43
|
+
"src/",
|
|
44
|
+
"!src/**/*.test.ts",
|
|
45
|
+
"!src/test-setup.ts",
|
|
46
|
+
"!src/__fixtures__/",
|
|
47
|
+
"scripts/",
|
|
48
|
+
"openclaw.plugin.json",
|
|
49
|
+
"README.md",
|
|
50
|
+
"SKILL.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"openclaw": {
|
|
54
|
+
"extensions": [
|
|
55
|
+
"./dist/index.js"
|
|
56
|
+
],
|
|
57
|
+
"setupEntry": "./dist/setup-entry.js",
|
|
58
|
+
"channel": {
|
|
59
|
+
"id": "max",
|
|
60
|
+
"label": "MAX",
|
|
61
|
+
"selectionLabel": "MAX (max.ru)",
|
|
62
|
+
"docsPath": "/channels/max",
|
|
63
|
+
"docsLabel": "max",
|
|
64
|
+
"blurb": "MAX messenger channel (max.ru Bot API)."
|
|
65
|
+
},
|
|
66
|
+
"compat": {
|
|
67
|
+
"pluginApi": ">=2026.9.6",
|
|
68
|
+
"minGatewayVersion": "2026.9.6"
|
|
69
|
+
},
|
|
70
|
+
"build": {
|
|
71
|
+
"openclawVersion": "2026.9.6",
|
|
72
|
+
"pluginSdkVersion": "2026.9.6"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^25.5.0",
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
|
78
|
+
"@typescript-eslint/parser": "^8.61.0",
|
|
79
|
+
"eslint": "^9.39.4",
|
|
80
|
+
"eslint-config-prettier": "^10.1.8",
|
|
81
|
+
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
82
|
+
"openclaw": "^2026.9.6",
|
|
83
|
+
"prettier": "^3.8.4",
|
|
84
|
+
"typescript": "^5.9.3",
|
|
85
|
+
"vitest": "^4.1.8",
|
|
86
|
+
"yaml": "^2.9.1"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"openclaw": ">=2026.9.6"
|
|
90
|
+
},
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"openclaw": {
|
|
93
|
+
"optional": false
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"undici": "^8.7.0",
|
|
98
|
+
"zod": "^4.4.3"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Fails when src/*.ts local imports form a cycle. Type-only imports count too
|
|
2
|
+
// (pass --runtime to ignore them).
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const runtimeOnly = process.argv.includes("--runtime");
|
|
7
|
+
const dir = new URL("../src/", import.meta.url).pathname;
|
|
8
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
|
|
9
|
+
const graph = new Map();
|
|
10
|
+
const importRe = /^\s*(import|export)\s+(type\s+)?[^;]*?from\s+["']\.\/([\w.-]+)\.js["']/gms;
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
const text = readFileSync(join(dir, file), "utf8");
|
|
13
|
+
const deps = new Set();
|
|
14
|
+
for (const m of text.matchAll(importRe)) {
|
|
15
|
+
if (runtimeOnly && m[2]) continue;
|
|
16
|
+
deps.add(`${m[3]}.ts`);
|
|
17
|
+
}
|
|
18
|
+
graph.set(file, [...deps].filter((d) => files.includes(d)));
|
|
19
|
+
}
|
|
20
|
+
const cycles = [];
|
|
21
|
+
const state = new Map();
|
|
22
|
+
const stack = [];
|
|
23
|
+
const visit = (node) => {
|
|
24
|
+
state.set(node, 1);
|
|
25
|
+
stack.push(node);
|
|
26
|
+
for (const dep of graph.get(node) ?? []) {
|
|
27
|
+
if (state.get(dep) === 1) cycles.push([...stack.slice(stack.indexOf(dep)), dep].join(" → "));
|
|
28
|
+
else if (!state.has(dep)) visit(dep);
|
|
29
|
+
}
|
|
30
|
+
stack.pop();
|
|
31
|
+
state.set(node, 2);
|
|
32
|
+
};
|
|
33
|
+
for (const file of files) if (!state.has(file)) visit(file);
|
|
34
|
+
if (cycles.length) {
|
|
35
|
+
console.error(`import cycles (${runtimeOnly ? "runtime" : "all"} imports):\n${cycles.join("\n")}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
console.log(`no import cycles among ${files.length} modules (${runtimeOnly ? "runtime" : "all"} imports)`);
|