clawgram 2.23.0 → 2.25.0

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/dist/chunk.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TELEGRAM_CAPTION_LIMIT = exports.TELEGRAM_TEXT_LIMIT = void 0;
4
+ exports.chunkTelegramText = chunkTelegramText;
5
+ /**
6
+ * Telegram limits: 4096 characters per message, 1024 per media caption.
7
+ *
8
+ * Core chunks only the replies it dispatches itself; a `send` issued through
9
+ * the message tool — the way the guard scripts deliver reports — reaches
10
+ * GramJS whole, and GramJS does not split either, so a long report failed
11
+ * with MESSAGE_TOO_LONG and the agent saw "✉️ Message failed" while the
12
+ * human saw nothing (audit B5-03).
13
+ *
14
+ * Splitting prefers paragraph breaks, then line breaks, then a hard cut at
15
+ * the limit. It runs on the text as the agent wrote it — before HTML
16
+ * rendering — so a chunk boundary can only fall between the agent's own
17
+ * lines, never inside an entity GramJS produced.
18
+ */
19
+ exports.TELEGRAM_TEXT_LIMIT = 4096;
20
+ exports.TELEGRAM_CAPTION_LIMIT = 1024;
21
+ function chunkTelegramText(text, limit = exports.TELEGRAM_TEXT_LIMIT) {
22
+ const chars = Array.from(text);
23
+ if (chars.length <= limit)
24
+ return [text];
25
+ const chunks = [];
26
+ let rest = text;
27
+ while (Array.from(rest).length > limit) {
28
+ const window = Array.from(rest).slice(0, limit).join("");
29
+ let cut = window.lastIndexOf("\n\n");
30
+ if (cut < limit / 2)
31
+ cut = window.lastIndexOf("\n");
32
+ if (cut < limit / 2)
33
+ cut = window.lastIndexOf(" ");
34
+ if (cut < limit / 2)
35
+ cut = window.length;
36
+ const piece = rest.slice(0, cut).replace(/\s+$/u, "");
37
+ chunks.push(piece);
38
+ rest = rest.slice(cut).replace(/^\s+/u, "");
39
+ }
40
+ if (rest)
41
+ chunks.push(rest);
42
+ return chunks;
43
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GramJsClientManager = void 0;
4
4
  exports.buildParticipantsQuery = buildParticipantsQuery;
5
5
  exports.buildVoiceNoteParams = buildVoiceNoteParams;
6
+ const chunk_1 = require("./chunk");
6
7
  const telegram_1 = require("telegram");
7
8
  const sessions_1 = require("telegram/sessions");
8
9
  // Deep import, but the documented one: GramJS ships its SRP helper here and
@@ -313,7 +314,8 @@ class GramJsClientManager {
313
314
  // но теперь виден в логе: если он в логе частый, значит кэш не спасает и
314
315
  // адресация идёт не тем ключом (A6-14).
315
316
  peerLog.info("clawgram peer resolve falling back to dialog scan", {
316
- target: raw,
317
+ // Цель может быть телефоном или @handle человека — в журнал идёт только её форма (B5-09).
318
+ targetShape: String(raw).startsWith("+") ? "phone" : String(raw).startsWith("@") ? "handle" : /^-?\d+/.test(String(raw)) ? "id" : "other",
317
319
  kind: kind ?? null,
318
320
  });
319
321
  const dialogs = await this.client.getDialogs({ limit: 200 }).catch(() => []);
@@ -418,6 +420,17 @@ class GramJsClientManager {
418
420
  const resolved = await this.resolvePeer(args.target, { kind: args.targetKind });
419
421
  const messageThreadId = args.messageThreadId ?? resolved.messageThreadId;
420
422
  const replyParams = buildForumReplyParams(messageThreadId, args.replyToMessageId);
423
+ // Длиннее лимита Telegram — несколько сообщений подряд, а не
424
+ // MESSAGE_TOO_LONG (B5-03). Нарезка — по тексту агента, до рендера.
425
+ const chunks = (0, chunk_1.chunkTelegramText)(args.text, chunk_1.TELEGRAM_TEXT_LIMIT);
426
+ if (chunks.length > 1) {
427
+ let last;
428
+ for (const [index, chunk] of chunks.entries()) {
429
+ last = await this.sendText({ ...args, text: chunk,
430
+ ...(index > 0 ? { replyToMessageId: undefined } : {}) });
431
+ }
432
+ return last;
433
+ }
421
434
  return this.client.sendMessage(resolved.peer, {
422
435
  // In html mode the text is rendered first: the agent writes markdown,
423
436
  // Telegram HTML, or both, and GramJS's HTML parser alone would ship
@@ -757,6 +770,16 @@ class GramJsClientManager {
757
770
  const resolved = await this.resolvePeer(args.target);
758
771
  const messageThreadId = args.messageThreadId ?? resolved.messageThreadId;
759
772
  const replyParams = buildForumReplyParams(messageThreadId, args.replyToMessageId);
773
+ // Подпись длиннее 1024 — файл с первой частью, остальное текстом следом
774
+ // (B5-03); раньше вся подпись уезжала целиком и падала на лимите.
775
+ const captionChunks = args.caption ? (0, chunk_1.chunkTelegramText)(args.caption, chunk_1.TELEGRAM_CAPTION_LIMIT) : [];
776
+ if (captionChunks.length > 1) {
777
+ const sent = await this.sendMedia({ ...args, caption: captionChunks[0] });
778
+ for (const chunk of captionChunks.slice(1)) {
779
+ await this.sendText({ target: args.target, text: chunk, parseMode: args.parseMode, messageThreadId });
780
+ }
781
+ return sent;
782
+ }
760
783
  return this.client.sendFile(resolved.peer, {
761
784
  file: args.file,
762
785
  // Captions are agent prose too — the outbound path sends `caption ??
package/dist/helpers.js CHANGED
@@ -47,6 +47,8 @@ exports.resolveReplyParseMode = resolveReplyParseMode;
47
47
  exports.resolveOutboundParseMode = resolveOutboundParseMode;
48
48
  exports.resolveDryRun = resolveDryRun;
49
49
  exports.parseOptionalThreadId = parseOptionalThreadId;
50
+ exports.readAccountReactionLevel = readAccountReactionLevel;
51
+ exports.readAccountReactionModel = readAccountReactionModel;
50
52
  const node_fs_1 = require("node:fs");
51
53
  const node_path_1 = __importDefault(require("node:path"));
52
54
  const core_1 = require("openclaw/plugin-sdk/core");
@@ -904,3 +906,27 @@ function parseOptionalThreadId(value) {
904
906
  const parsed = Number.parseInt(trimmed, 10);
905
907
  return Number.isFinite(parsed) ? parsed : undefined;
906
908
  }
909
+ /** Reads the configured reaction level for an account, tolerating a missing config. */
910
+ function readAccountReactionLevel(cfg, accountId) {
911
+ const resolvedAccountId = resolveConfiguredAccountId(cfg, accountId);
912
+ if (!resolvedAccountId) {
913
+ return undefined;
914
+ }
915
+ return cfg?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionLevel;
916
+ }
917
+ /**
918
+ * Model ref for the emoji pick, when the account names one.
919
+ *
920
+ * Picking one emoji out of a fixed list of 68 is the cheapest judgement this
921
+ * channel makes and the only model call it makes on its own; running it on the
922
+ * agent's own head spends the expensive quota on a decision a small model
923
+ * makes just as well.
924
+ */
925
+ function readAccountReactionModel(cfg, accountId) {
926
+ const resolvedAccountId = resolveConfiguredAccountId(cfg, accountId);
927
+ if (!resolvedAccountId) {
928
+ return undefined;
929
+ }
930
+ const raw = cfg?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionModel;
931
+ return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
932
+ }