@aspalagin/openclaw-max 0.8.1 → 0.8.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/README.md +3 -1
- package/README_EN.md +3 -1
- package/assets/icon.png +0 -0
- package/dist/src/api.js +43 -4
- package/dist/src/deliver.js +3 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api.ts +43 -9
- package/src/deliver.ts +3 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Плагин нужен тем, кто пользуется OpenClaw и хочет, чтобы ассистент был доступен в MAX. Понадобятся токен бота MAX (ботов создают организации на [business.max.ru](https://business.max.ru/self)) и собственный gateway OpenClaw.
|
|
8
8
|
|
|
9
|
-
Версия 0.8.
|
|
9
|
+
Версия 0.8.2. Изменения: [CHANGELOG.md](https://github.com/aspalagin/openclaw-max/blob/main/CHANGELOG.md).
|
|
10
10
|
|
|
11
11
|
## Содержание
|
|
12
12
|
|
|
@@ -490,3 +490,5 @@ CI также запускает `typecheck` и тесты на `openclaw@latest
|
|
|
490
490
|
Разработка ведётся с помощью ИИ-агентов.
|
|
491
491
|
|
|
492
492
|
**Лицензия:** MIT, см. [LICENSE](LICENSE).
|
|
493
|
+
|
|
494
|
+
**Товарный знак.** Плагин неофициальный. MAX — товарный знак правообладателя, ООО «МАХ»; иконка пакета — логотип из [брендбука MAX](https://go.max.ru/brandbook).
|
package/README_EN.md
CHANGED
|
@@ -6,7 +6,7 @@ A channel plugin that connects an [OpenClaw](https://openclaw.ai) assistant to t
|
|
|
6
6
|
|
|
7
7
|
It is for OpenClaw users who need their assistant reachable in MAX. You need a MAX bot token (bots are created by organisations on [business.max.ru](https://business.max.ru/self)) and an OpenClaw gateway you run yourself.
|
|
8
8
|
|
|
9
|
-
Version 0.8.
|
|
9
|
+
Version 0.8.2. Changes: [CHANGELOG.md](https://github.com/aspalagin/openclaw-max/blob/main/CHANGELOG.md) (in Russian).
|
|
10
10
|
|
|
11
11
|
## Contents
|
|
12
12
|
|
|
@@ -490,3 +490,5 @@ Questions and proposals: [GitHub Issues](https://github.com/aspalagin/openclaw-m
|
|
|
490
490
|
Development is carried out with the help of AI agents.
|
|
491
491
|
|
|
492
492
|
**License:** MIT, see [LICENSE](LICENSE).
|
|
493
|
+
|
|
494
|
+
**Trademark.** This plugin is unofficial. MAX is a trademark of its rights holder, ООО «МАХ»; the package icon is the logo from the [MAX brand book](https://go.max.ru/brandbook).
|
package/assets/icon.png
CHANGED
|
Binary file
|
package/dist/src/api.js
CHANGED
|
@@ -200,6 +200,40 @@ export class MaxApiError extends Error {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
+
/** A MAX error code as it may appear in the log: a short dotted identifier. */
|
|
204
|
+
const LOGGABLE_ERROR_CODE = /^[A-Za-z0-9][\w.-]{0,63}$/;
|
|
205
|
+
/** A localization key MAX puts in `message`, e.g. "Key: errors.process.attachment.movie.not.owner". */
|
|
206
|
+
const LOGGABLE_ERROR_KEY = /^Key:\s*(errors\.[\w.-]{1,100})$/;
|
|
207
|
+
/**
|
|
208
|
+
* The machine-readable part of a MAX error response for the log: `code`
|
|
209
|
+
* (e.g. "proto.payload") and, when `message` is a bare localization key, that
|
|
210
|
+
* key. Free-form `message` text is left out: MAX documents it only as a
|
|
211
|
+
* human-readable description and some messages quote the request (e.g. "Path
|
|
212
|
+
* /chats is not recognized"), so it may echo user data. Accepts the parsed
|
|
213
|
+
* body or the raw response text; an empty or non-JSON body yields "".
|
|
214
|
+
* @internal exported for testing.
|
|
215
|
+
*/
|
|
216
|
+
export function describeMaxErrorBody(body) {
|
|
217
|
+
let parsed = body;
|
|
218
|
+
if (typeof body === 'string') {
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(body);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!parsed || typeof parsed !== 'object')
|
|
227
|
+
return '';
|
|
228
|
+
const { code, message } = parsed;
|
|
229
|
+
const parts = [];
|
|
230
|
+
if (typeof code === 'string' && LOGGABLE_ERROR_CODE.test(code))
|
|
231
|
+
parts.push(`code=${code}`);
|
|
232
|
+
const key = typeof message === 'string' ? LOGGABLE_ERROR_KEY.exec(message.trim())?.[1] : undefined;
|
|
233
|
+
if (key)
|
|
234
|
+
parts.push(`key=${key}`);
|
|
235
|
+
return parts.join(' ');
|
|
236
|
+
}
|
|
203
237
|
/**
|
|
204
238
|
* A request that hit the client-side deadline (our AbortController), as opposed
|
|
205
239
|
* to a caller-supplied stop signal. Carries the stalled phase so the send path
|
|
@@ -325,9 +359,12 @@ export class MaxApi {
|
|
|
325
359
|
}
|
|
326
360
|
if (!res.ok) {
|
|
327
361
|
// Never log the request body: it may carry the webhook secret (POST
|
|
328
|
-
// /subscriptions) or private message text. Log only method+path+status
|
|
329
|
-
|
|
330
|
-
const
|
|
362
|
+
// /subscriptions) or private message text. Log only method+path+status
|
|
363
|
+
// and the MAX error code (describeMaxErrorBody).
|
|
364
|
+
const detail = describeMaxErrorBody(json);
|
|
365
|
+
const summary = `${method} ${path} → ${res.status}${detail ? ` (${detail})` : ''}`;
|
|
366
|
+
getMaxLogger().error(`[MAX API] ${summary}`);
|
|
367
|
+
const error = new MaxApiError(`MAX API ${summary}`, res.status, json);
|
|
331
368
|
const retryAfter = res.headers?.get?.('retry-after');
|
|
332
369
|
if (retryAfter) {
|
|
333
370
|
const seconds = Number(retryAfter);
|
|
@@ -534,7 +571,9 @@ export class MaxApi {
|
|
|
534
571
|
body: multipart.body,
|
|
535
572
|
}, this.network().proxyUrl);
|
|
536
573
|
if (!uploadRes.ok) {
|
|
537
|
-
|
|
574
|
+
const text = await uploadRes.text().catch(() => null);
|
|
575
|
+
const detail = describeMaxErrorBody(text);
|
|
576
|
+
throw new MaxApiError(`MAX media upload failed: ${uploadRes.status}${detail ? ` (${detail})` : ''}`, uploadRes.status, text);
|
|
538
577
|
}
|
|
539
578
|
// Step 3: Parse response to extract token
|
|
540
579
|
// Image response: { photos: { "<id>": { token: "...", url: "..." } } }
|
package/dist/src/deliver.js
CHANGED
|
@@ -42,8 +42,9 @@ export function toMaxDeliveryError(err, visible) {
|
|
|
42
42
|
return err;
|
|
43
43
|
}
|
|
44
44
|
function logSendFailure(account, log, what, err) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// A MaxApiError message already carries the MAX error code; the response body
|
|
46
|
+
// stays out of the log, its free-form `message` may echo user data.
|
|
47
|
+
log?.error(`[${account.accountId}] MAX ${what} failed: ${String(err)}`);
|
|
47
48
|
}
|
|
48
49
|
export async function deliverMaxReply(params) {
|
|
49
50
|
const { payload, account, chatId, config, log, statusSink } = params;
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-max",
|
|
3
3
|
"name": "MAX",
|
|
4
4
|
"description": "MAX messenger (max.ru) channel for OpenClaw: private and group chats over webhook or long polling; media, voice messages with transcription and voice replies; inline keyboards and command menus; message edits and pins; several bot accounts",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.2",
|
|
6
6
|
"categories": [
|
|
7
7
|
"channels"
|
|
8
8
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspalagin/openclaw-max",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "MAX messenger (max.ru) channel for OpenClaw: private and group chats over webhook or long polling; media, voice messages with transcription and voice replies; inline keyboards and command menus; message edits and pins; several bot accounts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
package/src/api.ts
CHANGED
|
@@ -320,6 +320,39 @@ export class MaxApiError extends Error {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/** A MAX error code as it may appear in the log: a short dotted identifier. */
|
|
324
|
+
const LOGGABLE_ERROR_CODE = /^[A-Za-z0-9][\w.-]{0,63}$/;
|
|
325
|
+
/** A localization key MAX puts in `message`, e.g. "Key: errors.process.attachment.movie.not.owner". */
|
|
326
|
+
const LOGGABLE_ERROR_KEY = /^Key:\s*(errors\.[\w.-]{1,100})$/;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The machine-readable part of a MAX error response for the log: `code`
|
|
330
|
+
* (e.g. "proto.payload") and, when `message` is a bare localization key, that
|
|
331
|
+
* key. Free-form `message` text is left out: MAX documents it only as a
|
|
332
|
+
* human-readable description and some messages quote the request (e.g. "Path
|
|
333
|
+
* /chats is not recognized"), so it may echo user data. Accepts the parsed
|
|
334
|
+
* body or the raw response text; an empty or non-JSON body yields "".
|
|
335
|
+
* @internal exported for testing.
|
|
336
|
+
*/
|
|
337
|
+
export function describeMaxErrorBody(body: unknown): string {
|
|
338
|
+
let parsed = body;
|
|
339
|
+
if (typeof body === 'string') {
|
|
340
|
+
try {
|
|
341
|
+
parsed = JSON.parse(body);
|
|
342
|
+
} catch {
|
|
343
|
+
return '';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!parsed || typeof parsed !== 'object') return '';
|
|
347
|
+
const { code, message } = parsed as { code?: unknown; message?: unknown };
|
|
348
|
+
const parts: string[] = [];
|
|
349
|
+
if (typeof code === 'string' && LOGGABLE_ERROR_CODE.test(code)) parts.push(`code=${code}`);
|
|
350
|
+
const key =
|
|
351
|
+
typeof message === 'string' ? LOGGABLE_ERROR_KEY.exec(message.trim())?.[1] : undefined;
|
|
352
|
+
if (key) parts.push(`key=${key}`);
|
|
353
|
+
return parts.join(' ');
|
|
354
|
+
}
|
|
355
|
+
|
|
323
356
|
/**
|
|
324
357
|
* A request that hit the client-side deadline (our AbortController), as opposed
|
|
325
358
|
* to a caller-supplied stop signal. Carries the stalled phase so the send path
|
|
@@ -462,13 +495,12 @@ export class MaxApi {
|
|
|
462
495
|
|
|
463
496
|
if (!res.ok) {
|
|
464
497
|
// Never log the request body: it may carry the webhook secret (POST
|
|
465
|
-
// /subscriptions) or private message text. Log only method+path+status
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
);
|
|
498
|
+
// /subscriptions) or private message text. Log only method+path+status
|
|
499
|
+
// and the MAX error code (describeMaxErrorBody).
|
|
500
|
+
const detail = describeMaxErrorBody(json);
|
|
501
|
+
const summary = `${method} ${path} → ${res.status}${detail ? ` (${detail})` : ''}`;
|
|
502
|
+
getMaxLogger().error(`[MAX API] ${summary}`);
|
|
503
|
+
const error = new MaxApiError(`MAX API ${summary}`, res.status, json);
|
|
472
504
|
const retryAfter = res.headers?.get?.('retry-after');
|
|
473
505
|
if (retryAfter) {
|
|
474
506
|
const seconds = Number(retryAfter);
|
|
@@ -769,10 +801,12 @@ export class MaxApi {
|
|
|
769
801
|
);
|
|
770
802
|
|
|
771
803
|
if (!uploadRes.ok) {
|
|
804
|
+
const text = await uploadRes.text().catch(() => null);
|
|
805
|
+
const detail = describeMaxErrorBody(text);
|
|
772
806
|
throw new MaxApiError(
|
|
773
|
-
`MAX media upload failed: ${uploadRes.status}`,
|
|
807
|
+
`MAX media upload failed: ${uploadRes.status}${detail ? ` (${detail})` : ''}`,
|
|
774
808
|
uploadRes.status,
|
|
775
|
-
|
|
809
|
+
text,
|
|
776
810
|
);
|
|
777
811
|
}
|
|
778
812
|
|
package/src/deliver.ts
CHANGED
|
@@ -72,10 +72,9 @@ function logSendFailure(
|
|
|
72
72
|
what: string,
|
|
73
73
|
err: unknown,
|
|
74
74
|
): void {
|
|
75
|
-
|
|
76
|
-
log
|
|
77
|
-
|
|
78
|
-
);
|
|
75
|
+
// A MaxApiError message already carries the MAX error code; the response body
|
|
76
|
+
// stays out of the log, its free-form `message` may echo user data.
|
|
77
|
+
log?.error(`[${account.accountId}] MAX ${what} failed: ${String(err)}`);
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
export async function deliverMaxReply(params: {
|