clawgram 2.6.1 → 2.7.1
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/channel.js +53 -11
- package/dist/gramjs-client.js +18 -0
- package/dist/helpers.js +35 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -12,6 +12,36 @@ const node_fs_1 = require("node:fs");
|
|
|
12
12
|
* a different conversation from a spoken line or a screenshot, and the
|
|
13
13
|
* transfer is not free. */
|
|
14
14
|
const INBOUND_MEDIA_MAX_BYTES = 25 * 1024 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* What this channel promises the Gateway.
|
|
17
|
+
*
|
|
18
|
+
* Annotated with core's own `ChannelCapabilities` on purpose: the shape is read
|
|
19
|
+
* by core (`resolveChannelTtsVoiceDelivery` reaches straight into
|
|
20
|
+
* `capabilities.tts.voice`), so a typo here would not fail — it would silently
|
|
21
|
+
* fall back to a default. With the annotation the compiler checks the promise
|
|
22
|
+
* against the version of OpenClaw we build against.
|
|
23
|
+
*/
|
|
24
|
+
const CHANNEL_CAPABILITIES = {
|
|
25
|
+
chatTypes: ["direct", "group"],
|
|
26
|
+
reactions: true,
|
|
27
|
+
threads: true,
|
|
28
|
+
media: true,
|
|
29
|
+
nativeCommands: false,
|
|
30
|
+
blockStreaming: false,
|
|
31
|
+
// Without this key core resolves the default "audio-file" and delivers
|
|
32
|
+
// synthesized speech as a document: a grey file card you must download
|
|
33
|
+
// before you know what it is. Advertising "voice-note" makes core mark such
|
|
34
|
+
// sends with `asVoice`, which the upload path honours.
|
|
35
|
+
//
|
|
36
|
+
// `transcodesAudio` is deliberately absent: we ship no ffmpeg and add no
|
|
37
|
+
// dependencies, so core must hand us Ogg/Opus — the only container Telegram
|
|
38
|
+
// renders as a voice bubble.
|
|
39
|
+
tts: {
|
|
40
|
+
voice: {
|
|
41
|
+
synthesisTarget: "voice-note",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
15
45
|
const media_1 = require("./media");
|
|
16
46
|
const channel_runtime_1 = require("openclaw/plugin-sdk/channel-runtime");
|
|
17
47
|
const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
|
|
@@ -201,14 +231,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
201
231
|
blurb: "Connect your personal Telegram account to OpenClaw via MTProto. Your AI assistant responds as you.",
|
|
202
232
|
aliases: ["tguserbot"],
|
|
203
233
|
},
|
|
204
|
-
capabilities:
|
|
205
|
-
chatTypes: ["direct", "group"],
|
|
206
|
-
reactions: true,
|
|
207
|
-
threads: true,
|
|
208
|
-
media: true,
|
|
209
|
-
nativeCommands: false,
|
|
210
|
-
blockStreaming: false,
|
|
211
|
-
},
|
|
234
|
+
capabilities: CHANNEL_CAPABILITIES,
|
|
212
235
|
agentPrompt: {
|
|
213
236
|
messageToolHints: () => [
|
|
214
237
|
"Use clawgram to send Telegram replies from the connected personal account.",
|
|
@@ -758,7 +781,16 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
758
781
|
// A suppressed silent reply legitimately delivers nothing, so
|
|
759
782
|
// this fallback fires right after it. Without the same check
|
|
760
783
|
// the token would be read back from the transcript and sent.
|
|
761
|
-
|
|
784
|
+
//
|
|
785
|
+
// TTS markup needs the same treatment for the same reason:
|
|
786
|
+
// core strips it on the normal reply path, but this text comes
|
|
787
|
+
// straight out of the transcript. On 2026-08-08 a group got
|
|
788
|
+
// `[[tts:text]]Привет, Вася!…[[/tts:text]]` verbatim. The
|
|
789
|
+
// spoken words are kept — a synthesis that did not happen
|
|
790
|
+
// should degrade to readable text, not to markup.
|
|
791
|
+
const visibleFallbackText = fallbackText
|
|
792
|
+
? (0, helpers_1.stripTtsDirectives)((0, helpers_1.stripSilentReplyToken)(fallbackText))
|
|
793
|
+
: "";
|
|
762
794
|
if (fallbackText && !visibleFallbackText) {
|
|
763
795
|
log?.info?.("clawgram skipping silent transcript fallback", {
|
|
764
796
|
accountId,
|
|
@@ -1346,6 +1378,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1346
1378
|
: captionText.replaceAll("\\n", "\n");
|
|
1347
1379
|
const uploadReplyToId = (0, param_readers_1.readStringOrNumberParam)(params, "replyToId") ?? (0, param_readers_1.readStringOrNumberParam)(params, "replyTo");
|
|
1348
1380
|
const uploadThreadId = (0, param_readers_1.readStringOrNumberParam)(params, "threadId");
|
|
1381
|
+
const asVoice = (0, helpers_1.readVoiceNoteFlag)(params);
|
|
1349
1382
|
actionLog.info("clawgram handleAction upload-file", {
|
|
1350
1383
|
accountId: uploadAccountId,
|
|
1351
1384
|
dryRun: dryRun === true,
|
|
@@ -1353,6 +1386,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1353
1386
|
hasCaption: Boolean(caption),
|
|
1354
1387
|
replyToId: uploadReplyToId ?? null,
|
|
1355
1388
|
threadId: uploadThreadId ?? null,
|
|
1389
|
+
asVoice,
|
|
1356
1390
|
});
|
|
1357
1391
|
if (dryRun === true) {
|
|
1358
1392
|
return (0, core_1.jsonResult)({
|
|
@@ -1372,6 +1406,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1372
1406
|
caption: caption || undefined,
|
|
1373
1407
|
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(rawUploadTo, uploadReplyToId),
|
|
1374
1408
|
messageThreadId: parseOptionalThreadId(uploadThreadId),
|
|
1409
|
+
asVoice,
|
|
1375
1410
|
});
|
|
1376
1411
|
actionLog.info("clawgram handleAction upload-file completed", {
|
|
1377
1412
|
accountId: uploadAccountId,
|
|
@@ -1608,25 +1643,32 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1608
1643
|
}
|
|
1609
1644
|
actionLog.info("clawgram outbound sendMedia", {
|
|
1610
1645
|
accountId: ctx.accountId,
|
|
1611
|
-
|
|
1646
|
+
rawTo: ctx.to,
|
|
1612
1647
|
replyToId: ctx.replyToId ?? null,
|
|
1613
1648
|
threadId: ctx.threadId ?? null,
|
|
1614
1649
|
filePath: ctx.filePath ?? null,
|
|
1615
1650
|
mediaUrl: ctx.mediaUrl ?? null,
|
|
1616
1651
|
hasText: Boolean(ctx.text),
|
|
1617
1652
|
hasCaption: Boolean(ctx.caption),
|
|
1653
|
+
asVoice: ctx.audioAsVoice === true,
|
|
1618
1654
|
});
|
|
1619
1655
|
const file = ctx.filePath ?? ctx.mediaUrl;
|
|
1620
1656
|
if (!file) {
|
|
1621
1657
|
throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
|
|
1622
1658
|
}
|
|
1623
1659
|
const messageThreadId = parseOptionalThreadId(ctx.threadId);
|
|
1660
|
+
// Same normalization `sendText` does two functions up. Without it the
|
|
1661
|
+
// channel prefix reaches peer resolution and the send throws — which is
|
|
1662
|
+
// exactly how a synthesized group reply died on 2026-08-08, silently
|
|
1663
|
+
// enough that the transcript fallback posted it as raw text instead.
|
|
1664
|
+
const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
1624
1665
|
const sent = await gram.sendMedia({
|
|
1625
|
-
target
|
|
1666
|
+
target,
|
|
1626
1667
|
file,
|
|
1627
1668
|
caption: ctx.caption ?? ctx.text,
|
|
1628
1669
|
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
|
|
1629
1670
|
messageThreadId,
|
|
1671
|
+
asVoice: ctx.audioAsVoice === true,
|
|
1630
1672
|
});
|
|
1631
1673
|
actionLog.info("clawgram outbound sendMedia completed", {
|
|
1632
1674
|
accountId: ctx.accountId,
|
package/dist/gramjs-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GramJsClientManager = void 0;
|
|
4
|
+
exports.buildVoiceNoteParams = buildVoiceNoteParams;
|
|
4
5
|
const telegram_1 = require("telegram");
|
|
5
6
|
const sessions_1 = require("telegram/sessions");
|
|
6
7
|
const helpers_1 = require("./helpers");
|
|
@@ -79,6 +80,22 @@ function parseTargetWithThread(rawTarget) {
|
|
|
79
80
|
chatId: raw,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Voice-message option for `sendFile`.
|
|
85
|
+
*
|
|
86
|
+
* GramJS branches on `voiceNote` and builds `DocumentAttributeAudio` with
|
|
87
|
+
* `voice: true` itself, so the attribute must not be assembled by hand.
|
|
88
|
+
* The key is omitted rather than set to `false`: this codebase already paid
|
|
89
|
+
* for the lesson that a key's mere presence can flip a branch (see the MTProxy
|
|
90
|
+
* note in `proxy-config.ts`).
|
|
91
|
+
*
|
|
92
|
+
* Telegram renders a voice bubble only for Ogg/Opus; core is responsible for
|
|
93
|
+
* handing us that container, which is why the channel does not advertise
|
|
94
|
+
* `transcodesAudio`.
|
|
95
|
+
*/
|
|
96
|
+
function buildVoiceNoteParams(asVoice) {
|
|
97
|
+
return asVoice === true ? { voiceNote: true } : {};
|
|
98
|
+
}
|
|
82
99
|
function buildForumReplyParams(messageThreadId, replyToMessageId) {
|
|
83
100
|
const normalizedThreadId = typeof messageThreadId === "number" && Number.isFinite(messageThreadId)
|
|
84
101
|
? Math.trunc(messageThreadId)
|
|
@@ -502,6 +519,7 @@ class GramJsClientManager {
|
|
|
502
519
|
file: args.file,
|
|
503
520
|
caption: args.caption,
|
|
504
521
|
...replyParams,
|
|
522
|
+
...buildVoiceNoteParams(args.asVoice),
|
|
505
523
|
});
|
|
506
524
|
}
|
|
507
525
|
}
|
package/dist/helpers.js
CHANGED
|
@@ -15,6 +15,8 @@ exports.readLatestAssistantFallbackFromTranscript = readLatestAssistantFallbackF
|
|
|
15
15
|
exports.resolveActionTarget = resolveActionTarget;
|
|
16
16
|
exports.resolveReplyToMessageIdForTarget = resolveReplyToMessageIdForTarget;
|
|
17
17
|
exports.readMessageText = readMessageText;
|
|
18
|
+
exports.readVoiceNoteFlag = readVoiceNoteFlag;
|
|
19
|
+
exports.stripTtsDirectives = stripTtsDirectives;
|
|
18
20
|
exports.resolveAllowFrom = resolveAllowFrom;
|
|
19
21
|
exports.resolveGroupPolicy = resolveGroupPolicy;
|
|
20
22
|
exports.resolveGroups = resolveGroups;
|
|
@@ -193,6 +195,39 @@ function resolveReplyToMessageIdForTarget(rawTarget, replyToId) {
|
|
|
193
195
|
}
|
|
194
196
|
return undefined;
|
|
195
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Разметка синтеза речи, которая не должна доехать до человека.
|
|
200
|
+
*
|
|
201
|
+
* Core вырезает `[[tts:...]]` из видимого текста сам, но только на штатном
|
|
202
|
+
* пути ответа. Аварийный путь (`readLatestAssistantFallbackFromTranscript`)
|
|
203
|
+
* читает сырой текст из стенограммы, поэтому 2026-08-08 в групповой чат
|
|
204
|
+
* ушло `[[tts:text]]Привет, Вася!…[[/tts:text]]` как есть.
|
|
205
|
+
*
|
|
206
|
+
* Блок `[[tts:text]]…[[/tts:text]]` РАЗВОРАЧИВАЕТСЯ, а не удаляется: внутри
|
|
207
|
+
* лежит то, что агент собирался сказать. Если синтез не состоялся, человек
|
|
208
|
+
* должен получить эти слова текстом — деградация в читаемое, а не в мусор
|
|
209
|
+
* и не в пустоту.
|
|
210
|
+
*/
|
|
211
|
+
const TTS_TEXT_BLOCK = /\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi;
|
|
212
|
+
const TTS_DIRECTIVE = /\[\[\s*\/?\s*(?:tts:[^\]]*|audio_as_voice)\s*\]\]/gi;
|
|
213
|
+
function stripTtsDirectives(text) {
|
|
214
|
+
return text
|
|
215
|
+
.replace(TTS_TEXT_BLOCK, (_match, spoken) => spoken)
|
|
216
|
+
.replace(TTS_DIRECTIVE, "")
|
|
217
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
218
|
+
.trim();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Whether an outbound file should become a Telegram voice message.
|
|
222
|
+
*
|
|
223
|
+
* Core emits `asVoice` and, on some paths, the older `audioAsVoice` — both
|
|
224
|
+
* carry the same meaning, so both are read. Only a real `true` counts: a
|
|
225
|
+
* string "true" or a stray truthy value must not silently turn a document
|
|
226
|
+
* into a voice bubble.
|
|
227
|
+
*/
|
|
228
|
+
function readVoiceNoteFlag(params) {
|
|
229
|
+
return params?.asVoice === true || params?.audioAsVoice === true;
|
|
230
|
+
}
|
|
196
231
|
function readMessageText(params) {
|
|
197
232
|
const message = (0, core_1.readStringParam)(params, "message", { allowEmpty: true });
|
|
198
233
|
if (typeof message === "string") {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "clawgram",
|
|
3
3
|
"name": "Clawgram",
|
|
4
4
|
"description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.7.1",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED