clawgram 2.20.0 → 2.21.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/README.md +21 -4
- package/dist/channel.js +114 -28
- package/dist/chat-info.js +1 -1
- package/dist/cli-core.js +25 -6
- package/dist/constants.js +9 -1
- package/dist/fetch-media.js +2 -1
- package/dist/gramjs-client.js +26 -6
- package/dist/group-reply-address.js +18 -2
- package/dist/helpers.js +151 -21
- package/dist/history.js +11 -2
- package/dist/manage.js +2 -1
- package/dist/media.js +56 -0
- package/dist/reactions.js +2 -1
- package/dist/topics.js +2 -1
- package/dist/update-config.js +22 -2
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Follow the steps in the console
|
|
|
94
94
|
```bash
|
|
95
95
|
Starting Clawgram authorization...
|
|
96
96
|
Please enter your apiId: 12345678
|
|
97
|
-
Please enter your apiHash:
|
|
97
|
+
Please enter your apiHash: EXAMPLE0123456789abcdef01234567
|
|
98
98
|
[2026-05-10T16:01:24.570] [INFO] - [Running gramJS version 2.26.21]
|
|
99
99
|
[2026-05-10T16:01:24.578] [INFO] - [Connecting to x.x.x.x:80/TCPFull...]
|
|
100
100
|
[2026-05-10T16:01:25.804] [INFO] - [Connection to x.x.x.x:80/TCPFull complete!]
|
|
@@ -191,6 +191,23 @@ config watcher and restart only this channel (a few seconds of MTProto
|
|
|
191
191
|
reconnect), not the whole Gateway.
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
## Upgrading to 2.21.0
|
|
195
|
+
|
|
196
|
+
One config key changed meaning: **`allowFrom: []` now denies everyone.** It used
|
|
197
|
+
to resolve to the wildcard, so emptying the list to shut an account off opened it
|
|
198
|
+
to every Telegram user instead — the opposite of the request, and the opposite of
|
|
199
|
+
`readChats: []` and `manageChats: []`, which have always been documented as deny.
|
|
200
|
+
|
|
201
|
+
An absent key still means everyone, so an install that never set `allowFrom` is
|
|
202
|
+
unaffected; only an explicitly empty list changes. If you relied on `[]` to admit
|
|
203
|
+
everyone, write `["*"]`. The account logs `clawgram allowFrom is empty: no direct
|
|
204
|
+
message will be accepted` at start, so the new state is never silent.
|
|
205
|
+
|
|
206
|
+
This is a behaviour change to a config key and by the usual rule it would cost a
|
|
207
|
+
major bump. It ships as a minor deliberately: the plugin currently has one
|
|
208
|
+
operator, the value is almost certainly nowhere, and the warning makes the case
|
|
209
|
+
loud where it does occur.
|
|
210
|
+
|
|
194
211
|
## Configuration Reference
|
|
195
212
|
|
|
196
213
|
### JSON Reference
|
|
@@ -231,7 +248,7 @@ reconnect), not the whole Gateway.
|
|
|
231
248
|
| `apiId` | number | required | Telegram API ID |
|
|
232
249
|
| `apiHash` | string | required | Telegram API hash |
|
|
233
250
|
| `sessionString` | string | `""` | Authenticated StringSession |
|
|
234
|
-
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames for direct messages only |
|
|
251
|
+
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames for direct messages only. Three states: absent means everyone, `[]` denies everyone (a warning is logged at account start), a list allows those senders |
|
|
235
252
|
| `groups` | object | `{}` | Allowed groups map keyed by explicit group id or `*` |
|
|
236
253
|
| `proxy` | object | unset | Optional SOCKS4/SOCKS5 proxy for this account — see [Proxy (SOCKS4/SOCKS5)](#proxy-socks4socks5) |
|
|
237
254
|
| `manageChats` | string[] | unset | Chats the assistant may **manage** — see [Chat management](#chat-management). Absent or empty = management off; `["*"]` = every chat |
|
|
@@ -245,7 +262,7 @@ Group config fields:
|
|
|
245
262
|
|---|---|---|---|
|
|
246
263
|
| `enabled` | boolean | `true` | Enables or disables replies in the group |
|
|
247
264
|
| `groupPolicy` | `"open"` \| `"mention"` \| `"tag"` | `"mention"` | What wakes the agent here — see [What wakes the agent in a group](#what-wakes-the-agent-in-a-group) |
|
|
248
|
-
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames inside that group |
|
|
265
|
+
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames inside that group. `[]` denies everyone, same as the account-level key |
|
|
249
266
|
| `tools` | object | unset | `{ allow?, alsoAllow?, deny? }` — tool policy for this group; see [Per-group tools, skills and system prompt](#per-group-tools-skills-and-system-prompt) |
|
|
250
267
|
| `toolsBySender` | object | unset | Per-sender tool policy inside this group, keys `id:<id>`, `username:<handle>`, `name:<display>` or `*` |
|
|
251
268
|
| `skills` | string[] | unset | Skill allowlist for this group; `[]` = no skills here, unset = the agent's skills |
|
|
@@ -807,7 +824,7 @@ that means in practice, and what the code does about it:
|
|
|
807
824
|
| `apiHash`, `sessionString` | `openclaw.json`, or a secret store | Written there by `--auth`. Never logged. Since 2.1.0 the session string is not printed after login either — only shown, behind an explicit warning, if you decline the automatic config write. Since 2.2.0 both accept a **SecretRef** instead of a literal, so the credential need not sit in the config file at all |
|
|
808
825
|
| Proxy password | `accounts.*.proxy.password`, or a secret store | Also accepts a SecretRef since 2.2.0. Marked `sensitive` in `uiHints`; diagnostics say `socks4`/`socks5` and nothing more. An invalid proxy fails the account rather than falling back to a direct connection, which would leak the host IP to Telegram |
|
|
809
826
|
| Message bodies | channel logs | **Not logged.** Outbound sends record recipient, ids and `textLength`. Until 2.1.0 the full outbound text was written to the channel log — if you ran 2.0.x, treat those journal entries as containing private correspondence |
|
|
810
|
-
| Read scope | `accounts.*.readChats` | History, membership and attachment fetches are confined to the listed chats. Absent means no restriction; an empty array denies everything |
|
|
827
|
+
| Read scope | `accounts.*.readChats` | History, membership and attachment fetches are confined to the listed chats. Absent means no restriction; an empty array denies everything. Telegram's own service chat (`777000`, where login codes arrive) is refused unconditionally, including under a wildcard |
|
|
811
828
|
| Manage scope | `accounts.*.manageChats` | Creating groups, changing membership, admin rights, ownership and invite links are confined to the listed chats — and **off entirely** when the key is absent or empty (opposite default to `readChats`, because these actions change chats rather than read them) |
|
|
812
829
|
| 2FA password | `accounts.*.twoFaPassword`, or a secret store | Read only by `transferOwnership`, exchanged for an SRP proof in-process. Accepts a SecretRef since 2.12.0; `sensitive` in `uiHints`; on the forbidden-log-keys list the static tests enforce |
|
|
813
830
|
| Who may talk to it | `allowFrom`, `groups.*.groupPolicy` | Direct-message senders and group behaviour are allowlisted; `mention` limits group replies to explicit mentions |
|
package/dist/channel.js
CHANGED
|
@@ -63,6 +63,7 @@ const inbound_envelope_1 = require("openclaw/plugin-sdk/inbound-envelope");
|
|
|
63
63
|
const inbound_reply_dispatch_1 = require("openclaw/plugin-sdk/inbound-reply-dispatch");
|
|
64
64
|
const channel_pairing_1 = require("openclaw/plugin-sdk/channel-pairing");
|
|
65
65
|
const events_1 = require("telegram/events");
|
|
66
|
+
const constants_1 = require("./constants");
|
|
66
67
|
const gramjs_client_1 = require("./gramjs-client");
|
|
67
68
|
const normalize_1 = require("./normalize");
|
|
68
69
|
const history_1 = require("./history");
|
|
@@ -81,7 +82,7 @@ const group_reply_address_1 = require("./group-reply-address");
|
|
|
81
82
|
const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
|
|
82
83
|
const helpers_1 = require("./helpers");
|
|
83
84
|
const proxy_config_1 = require("./proxy-config");
|
|
84
|
-
const
|
|
85
|
+
const constants_2 = require("./constants");
|
|
85
86
|
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
86
87
|
/** Reads the configured reaction level for an account, tolerating a missing config. */
|
|
87
88
|
function readAccountReactionLevel(cfg, accountId) {
|
|
@@ -89,7 +90,7 @@ function readAccountReactionLevel(cfg, accountId) {
|
|
|
89
90
|
if (!resolvedAccountId) {
|
|
90
91
|
return undefined;
|
|
91
92
|
}
|
|
92
|
-
return cfg?.channels?.[
|
|
93
|
+
return cfg?.channels?.[constants_2.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionLevel;
|
|
93
94
|
}
|
|
94
95
|
/**
|
|
95
96
|
* Model ref for the emoji pick, when the account names one.
|
|
@@ -104,7 +105,7 @@ function readAccountReactionModel(cfg, accountId) {
|
|
|
104
105
|
if (!resolvedAccountId) {
|
|
105
106
|
return undefined;
|
|
106
107
|
}
|
|
107
|
-
const raw = cfg?.channels?.[
|
|
108
|
+
const raw = cfg?.channels?.[constants_2.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionModel;
|
|
108
109
|
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
109
110
|
}
|
|
110
111
|
/**
|
|
@@ -192,8 +193,10 @@ function readAccountManageChats(account) {
|
|
|
192
193
|
* from the same caller, on the same chat.
|
|
193
194
|
*
|
|
194
195
|
* Every name on the right maps to core target mode `"none"` except
|
|
195
|
-
* `channel-info`, which is `"
|
|
196
|
-
* `params.
|
|
196
|
+
* `channel-info`, which is `"channelId"`: the chat arrives in
|
|
197
|
+
* `params.channelId`, a spelling no parser here read until 2.21.0 — so the
|
|
198
|
+
* call fell through to the current chat and answered about the wrong one.
|
|
199
|
+
* `readChatTargetParam` is the single list of accepted spellings now.
|
|
197
200
|
*/
|
|
198
201
|
exports.CORE_ACTION_SYNONYMS = {
|
|
199
202
|
"thread-list": "topics",
|
|
@@ -480,8 +483,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
480
483
|
apiId: Number(account?.apiId),
|
|
481
484
|
apiHash: (0, secret_refs_1.readSecretInput)(account?.apiHash),
|
|
482
485
|
sessionString: (0, secret_refs_1.readSecretInput)(account?.sessionString),
|
|
483
|
-
|
|
484
|
-
groups: (0, helpers_1.resolveGroups)(account?.groups),
|
|
486
|
+
...(0, helpers_1.resolveAccountScopes)(cfg, accountId),
|
|
485
487
|
readChats: readAccountReadChats(account),
|
|
486
488
|
enabled: account?.enabled,
|
|
487
489
|
accountId,
|
|
@@ -505,6 +507,16 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
505
507
|
if (!channelRuntime) {
|
|
506
508
|
throw new Error("clawgram: channelRuntime is required");
|
|
507
509
|
}
|
|
510
|
+
// An empty allowlist denies everyone (2.21.0). That is the right
|
|
511
|
+
// default for a scope, but "the agent answers nobody" is indis-
|
|
512
|
+
// tinguishable from "the channel is broken" in a log, so say it out
|
|
513
|
+
// loud once per account start.
|
|
514
|
+
if (account.allowFrom.length === 0) {
|
|
515
|
+
log?.warn?.("clawgram allowFrom is empty: no direct message will be accepted", {
|
|
516
|
+
accountId,
|
|
517
|
+
hint: 'set allowFrom to ["*"] to accept everyone, or list the senders',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
508
520
|
if (runtimes.has(accountId)) {
|
|
509
521
|
log?.warn?.("clawgram stale runtime detected, reconnecting", { accountId });
|
|
510
522
|
await runtimes.get(accountId)?.stop().catch(() => undefined);
|
|
@@ -640,13 +652,41 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
640
652
|
return;
|
|
641
653
|
}
|
|
642
654
|
let text = normalized.text?.trim();
|
|
655
|
+
// Whether this sender may reach the agent at all — decided before
|
|
656
|
+
// the attachment is fetched.
|
|
657
|
+
//
|
|
658
|
+
// Reading an attachment downloads up to 25 MB and then spends a
|
|
659
|
+
// transcription or vision call on it. That used to happen for
|
|
660
|
+
// every photo and voice note from anyone in any group the account
|
|
661
|
+
// sits in, and only afterwards was the sender checked against
|
|
662
|
+
// `allowFrom`. A stranger could therefore spend the owner's model
|
|
663
|
+
// budget at will. None of these checks depend on the message text,
|
|
664
|
+
// so they cost nothing to run first.
|
|
665
|
+
const inboundSenderId = normalized.senderId ?? normalized.chatId;
|
|
666
|
+
const inboundScopes = (0, helpers_1.resolveAccountScopes)(cfg, accountId);
|
|
667
|
+
const inboundGroupConfig = normalized.chatType === "group"
|
|
668
|
+
? (0, helpers_1.resolveGroupConfig)(inboundScopes.groups, normalized.chatId)
|
|
669
|
+
: undefined;
|
|
670
|
+
const senderMayReachAgent = normalized.chatType === "group"
|
|
671
|
+
? Boolean(inboundGroupConfig
|
|
672
|
+
&& inboundGroupConfig.enabled !== false
|
|
673
|
+
&& (0, helpers_1.isSenderAllowed)({
|
|
674
|
+
allowFrom: inboundGroupConfig.allowFrom,
|
|
675
|
+
senderId: inboundSenderId,
|
|
676
|
+
senderUsername: normalized.senderUsername,
|
|
677
|
+
}))
|
|
678
|
+
: (0, helpers_1.isSenderAllowed)({
|
|
679
|
+
allowFrom: inboundScopes.allowFrom,
|
|
680
|
+
senderId: inboundSenderId,
|
|
681
|
+
senderUsername: normalized.senderUsername,
|
|
682
|
+
});
|
|
643
683
|
// An attachment carries no text of its own, and dropping it as
|
|
644
684
|
// "empty" is how the assistant used to go silent on being spoken
|
|
645
685
|
// to or shown something. Read it into the body instead: for a
|
|
646
686
|
// voice note and a screenshot alike, the attachment *is* the
|
|
647
687
|
// message. A caption is kept and the reading appended, because
|
|
648
688
|
// "look at this" plus the picture is one thought, not two.
|
|
649
|
-
const attachment = await readInboundAttachment({
|
|
689
|
+
const attachment = senderMayReachAgent ? await readInboundAttachment({
|
|
650
690
|
gram,
|
|
651
691
|
event,
|
|
652
692
|
cfg,
|
|
@@ -655,12 +695,25 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
655
695
|
accountId,
|
|
656
696
|
chatId: normalized.chatId,
|
|
657
697
|
messageId: normalized.messageId,
|
|
658
|
-
});
|
|
698
|
+
}) : undefined;
|
|
659
699
|
if (attachment) {
|
|
660
700
|
const marker = attachment.understanding === "transcript" ? "голосовое" : "изображение";
|
|
661
701
|
const read = `[${marker}] ${attachment.text}`;
|
|
662
702
|
text = text ? `${text}\n\n${read}` : read;
|
|
663
703
|
}
|
|
704
|
+
// What the mention gate is allowed to read.
|
|
705
|
+
//
|
|
706
|
+
// A transcript is the sender's own speech, so "Тина, посмотри"
|
|
707
|
+
// said aloud addresses the agent exactly as typing it would. A
|
|
708
|
+
// description is not: it is a vision model reading somebody
|
|
709
|
+
// else's content, and a screenshot of a chat where a third party
|
|
710
|
+
// wrote "@tina_bot" is not an address to her. Feeding the whole
|
|
711
|
+
// body to the gate made every such screenshot wake her up.
|
|
712
|
+
const addressableText = (0, helpers_1.resolveAddressableText)({
|
|
713
|
+
messageText: normalized.text,
|
|
714
|
+
bodyText: text,
|
|
715
|
+
understanding: attachment?.understanding,
|
|
716
|
+
});
|
|
664
717
|
if (!text) {
|
|
665
718
|
log?.info?.("clawgram skipping empty inbound text", {
|
|
666
719
|
accountId,
|
|
@@ -671,7 +724,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
671
724
|
}
|
|
672
725
|
const senderId = normalized.senderId ?? normalized.chatId;
|
|
673
726
|
const isTelegramServiceDirect = normalized.chatType === "direct" &&
|
|
674
|
-
(normalized.chatId ===
|
|
727
|
+
(normalized.chatId === constants_1.TELEGRAM_SERVICE_CHAT_ID || senderId === constants_1.TELEGRAM_SERVICE_CHAT_ID);
|
|
675
728
|
const isSavedMessagesDirect = normalized.chatType === "direct" &&
|
|
676
729
|
Boolean(selfId) &&
|
|
677
730
|
normalized.chatId === selfId &&
|
|
@@ -734,14 +787,13 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
734
787
|
}
|
|
735
788
|
throw lastError;
|
|
736
789
|
};
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
const groups = (0, helpers_1.resolveGroups)(accountConfig?.groups ?? account?.groups);
|
|
790
|
+
// Resolved once, above, before the attachment fetch that depends on
|
|
791
|
+
// the answer — and by the same resolver `resolveAccount` uses, so the
|
|
792
|
+
// gate applied here is the one the account was started with.
|
|
793
|
+
const { allowFrom: directAllowFrom } = inboundScopes;
|
|
742
794
|
const dmPolicy = "open";
|
|
743
795
|
if (normalized.chatType === "group") {
|
|
744
|
-
const groupConfig =
|
|
796
|
+
const groupConfig = inboundGroupConfig;
|
|
745
797
|
if (!groupConfig) {
|
|
746
798
|
log?.info?.("clawgram skipping group not present in groups config", {
|
|
747
799
|
accountId,
|
|
@@ -793,20 +845,24 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
793
845
|
// almost never. Only the `@` counts, and it is the same fact the
|
|
794
846
|
// stricter rung of the ladder is named after.
|
|
795
847
|
const wasMentioned = groupConfig.groupPolicy === "tag"
|
|
796
|
-
? (0, helpers_1.hasExplicitTelegramMention)({ selfUsername, text, message: rawMessage })
|
|
848
|
+
? (0, helpers_1.hasExplicitTelegramMention)({ selfUsername, text: addressableText, message: rawMessage })
|
|
797
849
|
: (0, helpers_1.hasTelegramMention)({
|
|
798
850
|
cfg,
|
|
799
851
|
agentId: route.agentId,
|
|
800
852
|
selfUsername,
|
|
801
|
-
text,
|
|
853
|
+
text: addressableText,
|
|
802
854
|
message: rawMessage,
|
|
803
855
|
});
|
|
804
|
-
|
|
856
|
+
// One fetch serves two needs: the reply-to-self gate below and
|
|
857
|
+
// the parent's text for the agent (ReplyToBody), which a plain
|
|
858
|
+
// reply does not carry on its own.
|
|
859
|
+
const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
|
|
860
|
+
const wasReplyToSelf = replyParent.isSelf;
|
|
805
861
|
const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
|
|
806
862
|
facts: {
|
|
807
863
|
canDetectMention: true,
|
|
808
864
|
wasMentioned,
|
|
809
|
-
hasAnyMention: /(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(
|
|
865
|
+
hasAnyMention: /(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(addressableText),
|
|
810
866
|
},
|
|
811
867
|
policy: {
|
|
812
868
|
isGroup: true,
|
|
@@ -827,7 +883,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
827
883
|
wasMentioned,
|
|
828
884
|
wasReplyToSelf,
|
|
829
885
|
shouldSkip: mentionDecision.shouldSkip,
|
|
830
|
-
text,
|
|
886
|
+
textLength: text.length,
|
|
831
887
|
});
|
|
832
888
|
if (groupConfig.groupPolicy !== "open" && mentionDecision.shouldSkip && !wasReplyToSelf) {
|
|
833
889
|
log?.info?.("clawgram skipping group message without mention", {
|
|
@@ -875,6 +931,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
875
931
|
// bare text, and the fragment the person pointed at is lost.
|
|
876
932
|
ReplyToQuoteText: normalized.replyQuoteText,
|
|
877
933
|
ReplyToIsQuote: normalized.replyIsQuote,
|
|
934
|
+
// A plain reply has no highlight; core then falls back to the
|
|
935
|
+
// parent's body, which only exists if the channel fetched it.
|
|
936
|
+
ReplyToBody: replyParent.body,
|
|
937
|
+
ReplyToSender: replyParent.sender,
|
|
878
938
|
MessageThreadId: normalized.messageThreadId,
|
|
879
939
|
NativeChannelId: normalized.chatId,
|
|
880
940
|
// Trusted per-group prompt block from `groups.<id>.systemPrompt`.
|
|
@@ -911,7 +971,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
911
971
|
ctx: ctxPayload,
|
|
912
972
|
updateLastRoute: {
|
|
913
973
|
sessionKey: route.sessionKey,
|
|
914
|
-
channel:
|
|
974
|
+
channel: constants_2.CHANNEL_ID,
|
|
915
975
|
to: conversationRouteTarget,
|
|
916
976
|
accountId: route.accountId ?? accountId,
|
|
917
977
|
},
|
|
@@ -954,7 +1014,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
954
1014
|
accountId,
|
|
955
1015
|
chatId: normalized.chatId,
|
|
956
1016
|
messageId: normalized.messageId,
|
|
957
|
-
|
|
1017
|
+
payloadTextLength: outboundText.length,
|
|
958
1018
|
payloadReplyToId: payload.replyToId ?? null,
|
|
959
1019
|
});
|
|
960
1020
|
if (!outboundText) {
|
|
@@ -1080,7 +1140,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1080
1140
|
chatId: normalized.chatId,
|
|
1081
1141
|
messageId: normalized.messageId,
|
|
1082
1142
|
routeSessionKey: route.sessionKey,
|
|
1083
|
-
|
|
1143
|
+
fallbackTextLength: visibleFallbackText.length,
|
|
1084
1144
|
});
|
|
1085
1145
|
await sendTextToConversation({
|
|
1086
1146
|
text: (0, helpers_1.prefixReplyTextToAddress)(visibleFallbackText, groupReplyAddress),
|
|
@@ -1179,6 +1239,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1179
1239
|
});
|
|
1180
1240
|
return;
|
|
1181
1241
|
}
|
|
1242
|
+
// Same fetch as the group path. In a DM the parent is as often
|
|
1243
|
+
// the agent's own message as the person's — the owner answers a
|
|
1244
|
+
// notice she sent — and neither text is available any other way.
|
|
1245
|
+
const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
|
|
1182
1246
|
await gram.withTyping(conversationTarget, async () => {
|
|
1183
1247
|
await (0, direct_dm_1.dispatchInboundDirectDmWithRuntime)({
|
|
1184
1248
|
cfg,
|
|
@@ -1210,6 +1274,8 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1210
1274
|
// direct messages too, and the fragment is not part of the text.
|
|
1211
1275
|
ReplyToQuoteText: normalized.replyQuoteText,
|
|
1212
1276
|
ReplyToIsQuote: normalized.replyIsQuote,
|
|
1277
|
+
ReplyToBody: replyParent.body,
|
|
1278
|
+
ReplyToSender: replyParent.sender,
|
|
1213
1279
|
NativeChannelId: normalized.chatId,
|
|
1214
1280
|
},
|
|
1215
1281
|
deliver: async (payload) => {
|
|
@@ -1324,7 +1390,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1324
1390
|
},
|
|
1325
1391
|
},
|
|
1326
1392
|
messaging: {
|
|
1327
|
-
targetPrefixes: [
|
|
1393
|
+
targetPrefixes: [constants_2.CHANNEL_ID, "tguserbot", "telegram", "tg"],
|
|
1328
1394
|
normalizeTarget(raw) {
|
|
1329
1395
|
const normalized = (0, helpers_1.normalizeOutboundTarget)(raw);
|
|
1330
1396
|
return normalized || undefined;
|
|
@@ -1396,7 +1462,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1396
1462
|
return (0, core_1.buildChannelOutboundSessionRoute)({
|
|
1397
1463
|
cfg: params.cfg,
|
|
1398
1464
|
agentId: params.agentId,
|
|
1399
|
-
channel:
|
|
1465
|
+
channel: constants_2.CHANNEL_ID,
|
|
1400
1466
|
accountId,
|
|
1401
1467
|
peer: {
|
|
1402
1468
|
kind: (0, helpers_1.routeKindFromChatType)(chatType),
|
|
@@ -1492,7 +1558,13 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1492
1558
|
"download-file": { aliases: ["chatId"] },
|
|
1493
1559
|
},
|
|
1494
1560
|
extractToolSend: ({ args }) => (0, tool_send_1.extractToolSend)(args, "sendMessage"),
|
|
1495
|
-
handleAction: async ({ action, params, cfg, accountId, dryRun: dryRunFlag, toolContext
|
|
1561
|
+
handleAction: async ({ action, params, cfg, accountId, dryRun: dryRunFlag, toolContext,
|
|
1562
|
+
// Core scopes every action to the media roots the agent may read, and
|
|
1563
|
+
// bundled channels enforce them. This one used to take `filePath`
|
|
1564
|
+
// verbatim, so a path naming the secret store or the config holding
|
|
1565
|
+
// `sessionString` was uploaded like any attachment.
|
|
1566
|
+
mediaLocalRoots, mediaAccess, }) => {
|
|
1567
|
+
const allowedMediaRoots = mediaLocalRoots ?? mediaAccess?.localRoots;
|
|
1496
1568
|
// Core passes the flag beside `params`; callers write it inside.
|
|
1497
1569
|
// Both count, because a rehearsal flag that is silently ignored puts
|
|
1498
1570
|
// a real message in a real chat — twice, so far (2.13.1).
|
|
@@ -2149,6 +2221,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
2149
2221
|
if (!file) {
|
|
2150
2222
|
throw new Error("clawgram: upload-file requires filePath, path, media, or mediaUrl");
|
|
2151
2223
|
}
|
|
2224
|
+
// Before anything else about the message is considered: an
|
|
2225
|
+
// out-of-scope path is refused, not sent and then regretted.
|
|
2226
|
+
(0, media_1.assertLocalMediaWithinRoots)(file, allowedMediaRoots);
|
|
2152
2227
|
const captionText = (0, helpers_1.readMessageText)(params) || ((0, param_readers_1.readStringParam)(params, "caption") ?? "");
|
|
2153
2228
|
// A caption is optional, but the silent-reply sentinel must never
|
|
2154
2229
|
// reach Telegram as one — same reasoning as the `send` path below.
|
|
@@ -2252,8 +2327,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
2252
2327
|
currentMessageId: String(currentMessageId),
|
|
2253
2328
|
toolContextCurrentChannelId: currentChannelId || null,
|
|
2254
2329
|
});
|
|
2330
|
+
// A dry run reports the suppression instead of impersonating it: the
|
|
2331
|
+
// caller asked what would happen, and what would happen is nothing.
|
|
2255
2332
|
return (0, core_1.jsonResult)({
|
|
2256
2333
|
ok: true,
|
|
2334
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
2257
2335
|
suppressedDuplicate: true,
|
|
2258
2336
|
to,
|
|
2259
2337
|
accountId: resolvedAccountId,
|
|
@@ -2264,7 +2342,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
2264
2342
|
// `replyToId`, and until 2026-08-10 that fell through to the most
|
|
2265
2343
|
// recent sender: in an interleaved chat the owner's report went out
|
|
2266
2344
|
// addressed to a colleague who had asked something else entirely.
|
|
2267
|
-
|
|
2345
|
+
// A dry run peeks: consuming the address here left the real send with
|
|
2346
|
+
// no greeting, so a rehearsal silently changed the message that went
|
|
2347
|
+
// out afterwards.
|
|
2348
|
+
const groupReplyAddress = (dryRun ? group_reply_address_1.peekGroupReplyAddress : group_reply_address_1.consumeGroupReplyAddress)({
|
|
2268
2349
|
accountId: resolvedAccountId,
|
|
2269
2350
|
chatId: to,
|
|
2270
2351
|
replyToId: replyToId ?? currentMessageId,
|
|
@@ -2483,6 +2564,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
2483
2564
|
if (!gram) {
|
|
2484
2565
|
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
2485
2566
|
}
|
|
2567
|
+
// Same rule as the action path: a local file outside the declared
|
|
2568
|
+
// roots is refused before anything is uploaded.
|
|
2569
|
+
const outboundRoots = ctx.mediaLocalRoots ?? ctx.mediaAccess?.localRoots;
|
|
2570
|
+
(0, media_1.assertLocalMediaWithinRoots)(ctx.filePath, outboundRoots);
|
|
2571
|
+
(0, media_1.assertLocalMediaWithinRoots)(ctx.mediaUrl, outboundRoots);
|
|
2486
2572
|
actionLog.info("clawgram outbound sendMedia", {
|
|
2487
2573
|
accountId: ctx.accountId,
|
|
2488
2574
|
rawTo: ctx.to,
|
package/dist/chat-info.js
CHANGED
|
@@ -19,7 +19,7 @@ exports.parseChatInfoParams = parseChatInfoParams;
|
|
|
19
19
|
exports.describeChat = describeChat;
|
|
20
20
|
const helpers_1 = require("./helpers");
|
|
21
21
|
function parseChatInfoParams(params, toolContext) {
|
|
22
|
-
const rawTarget =
|
|
22
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params, toolContext);
|
|
23
23
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
24
24
|
if (!target) {
|
|
25
25
|
throw new Error("clawgram: chatInfo requires a chatId");
|
package/dist/cli-core.js
CHANGED
|
@@ -10,6 +10,7 @@ const telegram_1 = require("telegram");
|
|
|
10
10
|
const sessions_1 = require("telegram/sessions");
|
|
11
11
|
const config_runtime_1 = require("openclaw/plugin-sdk/config-runtime");
|
|
12
12
|
const constants_1 = require("./constants");
|
|
13
|
+
const proxy_config_1 = require("./proxy-config");
|
|
13
14
|
const update_config_1 = require("./update-config");
|
|
14
15
|
function printRestartNotice() {
|
|
15
16
|
console.log("");
|
|
@@ -114,12 +115,29 @@ function buildConfigFragment(accountId, auth) {
|
|
|
114
115
|
},
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The proxy the account this login is for already uses, if any.
|
|
120
|
+
*
|
|
121
|
+
* The login handshake carries the phone number, the code and the 2FA password,
|
|
122
|
+
* and it used to go out from the host's real IP even on a deployment whose
|
|
123
|
+
* whole point is that Telegram never sees it: this flow built its own client
|
|
124
|
+
* and read no proxy at all. An unresolved SecretRef throws out of
|
|
125
|
+
* `buildTelegramClientOptions` rather than silently falling back to a direct
|
|
126
|
+
* connection — the same fail-closed rule the channel follows.
|
|
127
|
+
*/
|
|
128
|
+
function resolveAuthProxy(config, accountId) {
|
|
129
|
+
return config?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[accountId]?.proxy;
|
|
130
|
+
}
|
|
131
|
+
async function runTelegramAuthorization(prompt, proxy) {
|
|
118
132
|
const apiId = await prompt.askPositiveInteger("Please enter your apiId: ");
|
|
119
133
|
const apiHash = await prompt.askRequired("Please enter your apiHash: ");
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
const clientOptions = (0, proxy_config_1.buildTelegramClientOptions)(proxy);
|
|
135
|
+
if (clientOptions.proxy) {
|
|
136
|
+
// Scheme only. The host, port and credentials are exactly what must not
|
|
137
|
+
// reach a terminal, a screen share or CI scrollback.
|
|
138
|
+
console.log(`Connecting through the account's ${(0, proxy_config_1.describeProxy)(clientOptions.proxy)} proxy.`);
|
|
139
|
+
}
|
|
140
|
+
const client = new telegram_1.TelegramClient(new sessions_1.StringSession(""), apiId, apiHash, clientOptions);
|
|
123
141
|
try {
|
|
124
142
|
await client.start({
|
|
125
143
|
phoneNumber: async () => await prompt.askRequired("Please enter your number: "),
|
|
@@ -143,7 +161,8 @@ async function runTelegramUserbotAuth(config) {
|
|
|
143
161
|
const prompt = createPrompt();
|
|
144
162
|
try {
|
|
145
163
|
console.log("Starting Clawgram authorization...");
|
|
146
|
-
const
|
|
164
|
+
const authAccountId = resolveDefaultAccountId(config);
|
|
165
|
+
const auth = await runTelegramAuthorization(prompt, resolveAuthProxy(config, authAccountId));
|
|
147
166
|
console.log("Telegram authorization completed successfully.");
|
|
148
167
|
console.log("");
|
|
149
168
|
// The session string is a bearer secret for the whole Telegram account.
|
|
@@ -152,7 +171,7 @@ async function runTelegramUserbotAuth(config) {
|
|
|
152
171
|
// automatic config write and therefore has to paste it by hand.
|
|
153
172
|
console.log(`Session string received (${auth.sessionString.length} chars) — kept out of this output.`);
|
|
154
173
|
console.log("");
|
|
155
|
-
const defaultAccountId =
|
|
174
|
+
const defaultAccountId = authAccountId;
|
|
156
175
|
const rawAccountId = await prompt.ask(`Enter account id for config [${defaultAccountId}]: `);
|
|
157
176
|
const accountId = rawAccountId.trim() || defaultAccountId;
|
|
158
177
|
const shouldUpdateConfig = await prompt.askYesNo("Update OpenClaw config automatically? [y/N]: ", false);
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CLI_COMMAND = exports.CHANNEL_ID = void 0;
|
|
3
|
+
exports.TELEGRAM_SERVICE_CHAT_ID = exports.CLI_COMMAND = exports.CHANNEL_ID = void 0;
|
|
4
4
|
const CHANNEL_ID = "clawgram";
|
|
5
5
|
exports.CHANNEL_ID = CHANNEL_ID;
|
|
6
6
|
const CLI_COMMAND = "clawgram";
|
|
7
7
|
exports.CLI_COMMAND = CLI_COMMAND;
|
|
8
|
+
/**
|
|
9
|
+
* Telegram's own service account: login codes, password-reset notices and
|
|
10
|
+
* termination warnings arrive from it. Its messages are dropped on the way in
|
|
11
|
+
* and refused on the way out — a login code reaching the agent is an account
|
|
12
|
+
* takeover, not a privacy nuisance, so no `readChats` entry may enable it.
|
|
13
|
+
*/
|
|
14
|
+
const TELEGRAM_SERVICE_CHAT_ID = "777000";
|
|
15
|
+
exports.TELEGRAM_SERVICE_CHAT_ID = TELEGRAM_SERVICE_CHAT_ID;
|
package/dist/fetch-media.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.parseFetchMediaParams = parseFetchMediaParams;
|
|
|
21
21
|
exports.sanitizeFileName = sanitizeFileName;
|
|
22
22
|
exports.fetchedMediaFileName = fetchedMediaFileName;
|
|
23
23
|
const history_1 = require("./history");
|
|
24
|
+
const helpers_1 = require("./helpers");
|
|
24
25
|
/**
|
|
25
26
|
* A caller that guessed a neighbouring word is not refused: these are all
|
|
26
27
|
* unambiguous, and an error over vocabulary costs a turn to say nothing.
|
|
@@ -52,7 +53,7 @@ function parseFetchMediaMode(value) {
|
|
|
52
53
|
return mode;
|
|
53
54
|
}
|
|
54
55
|
function parseFetchMediaParams(params) {
|
|
55
|
-
const rawTarget =
|
|
56
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params);
|
|
56
57
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
57
58
|
if (!target) {
|
|
58
59
|
throw new Error("clawgram: fetch-media requires a chatId");
|
package/dist/gramjs-client.js
CHANGED
|
@@ -209,6 +209,7 @@ class GramJsClientManager {
|
|
|
209
209
|
client;
|
|
210
210
|
proxy;
|
|
211
211
|
started = false;
|
|
212
|
+
connected = false;
|
|
212
213
|
constructor(config) {
|
|
213
214
|
this.config = config;
|
|
214
215
|
// Credentials may be written as SecretRefs; account start-up resolves them
|
|
@@ -231,10 +232,24 @@ class GramJsClientManager {
|
|
|
231
232
|
async start() {
|
|
232
233
|
if (this.started)
|
|
233
234
|
return;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
// `connect()` starts the update loop before anything is known about the
|
|
236
|
+
// session, so a revoked or expired one used to throw with `started` still
|
|
237
|
+
// false — and `stop()`, which returned early on exactly that flag, then
|
|
238
|
+
// left a connected client retrying forever. That is the same leak 2.17.1
|
|
239
|
+
// fixed for the ordinary teardown path, reached through the failure path
|
|
240
|
+
// instead.
|
|
241
|
+
try {
|
|
242
|
+
await this.client.connect();
|
|
243
|
+
this.connected = true;
|
|
244
|
+
const authorized = await this.client.checkAuthorization();
|
|
245
|
+
if (!authorized) {
|
|
246
|
+
throw new Error("GramJS client connected, but session is not authorized.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
await this.client.destroy().catch(() => undefined);
|
|
251
|
+
this.connected = false;
|
|
252
|
+
throw error;
|
|
238
253
|
}
|
|
239
254
|
this.started = true;
|
|
240
255
|
}
|
|
@@ -254,10 +269,15 @@ class GramJsClientManager {
|
|
|
254
269
|
* before a restart, ~4.5/min after one.
|
|
255
270
|
*/
|
|
256
271
|
async stop() {
|
|
257
|
-
|
|
272
|
+
// Keyed on `connected`, not `started`: a client whose `connect()` succeeded
|
|
273
|
+
// and whose authorization then failed never set `started`, and the old
|
|
274
|
+
// guard let it keep its update loop. A client that never connected is
|
|
275
|
+
// still left alone, and a second `stop()` still destroys nothing.
|
|
276
|
+
if (!this.started && !this.connected)
|
|
258
277
|
return;
|
|
259
|
-
await this.client.destroy();
|
|
278
|
+
await this.client.destroy().catch(() => undefined);
|
|
260
279
|
this.started = false;
|
|
280
|
+
this.connected = false;
|
|
261
281
|
}
|
|
262
282
|
getClient() {
|
|
263
283
|
return this.client;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rememberGroupReplyAddress = rememberGroupReplyAddress;
|
|
4
4
|
exports.consumeGroupReplyAddress = consumeGroupReplyAddress;
|
|
5
|
+
exports.peekGroupReplyAddress = peekGroupReplyAddress;
|
|
5
6
|
exports.resetGroupReplyAddresses = resetGroupReplyAddresses;
|
|
6
7
|
exports.buildGroupReplyAddress = buildGroupReplyAddress;
|
|
7
8
|
const helpers_1 = require("./helpers");
|
|
@@ -45,7 +46,7 @@ function rememberGroupReplyAddress(input) {
|
|
|
45
46
|
* to "who am I replying to", so the fallback is gone — callers pass the message
|
|
46
47
|
* the turn is actually answering, and no message means no greeting.
|
|
47
48
|
*/
|
|
48
|
-
function
|
|
49
|
+
function readGroupReplyAddress(input, consume) {
|
|
49
50
|
const key = buildGroupReplyAddressKey(input);
|
|
50
51
|
if (!key) {
|
|
51
52
|
return undefined;
|
|
@@ -54,12 +55,27 @@ function consumeGroupReplyAddress(input) {
|
|
|
54
55
|
if (!stored) {
|
|
55
56
|
return undefined;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
if (consume) {
|
|
59
|
+
groupReplyAddresses.delete(key);
|
|
60
|
+
}
|
|
58
61
|
if (stored.expiresAt < Date.now()) {
|
|
59
62
|
return undefined;
|
|
60
63
|
}
|
|
61
64
|
return stored.address;
|
|
62
65
|
}
|
|
66
|
+
function consumeGroupReplyAddress(input) {
|
|
67
|
+
return readGroupReplyAddress(input, true);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The same lookup without the delete, for a dry run.
|
|
71
|
+
*
|
|
72
|
+
* A rehearsal that consumed the address changed what the real send would do:
|
|
73
|
+
* the greeting was gone by the time the message actually went out. A dry run
|
|
74
|
+
* has to answer "what would happen" without being the thing that happens.
|
|
75
|
+
*/
|
|
76
|
+
function peekGroupReplyAddress(input) {
|
|
77
|
+
return readGroupReplyAddress(input, false);
|
|
78
|
+
}
|
|
63
79
|
/** Test seam: the map is module state, and suites must not leak into each other. */
|
|
64
80
|
function resetGroupReplyAddresses() {
|
|
65
81
|
groupReplyAddresses.clear();
|
package/dist/helpers.js
CHANGED
|
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SILENT_REPLY_TOKEN = void 0;
|
|
6
|
+
exports.SILENT_REPLY_TOKEN = exports.CHAT_TARGET_PARAM_KEYS = void 0;
|
|
7
|
+
exports.resolveAddressableText = resolveAddressableText;
|
|
8
|
+
exports.readChatTargetParam = readChatTargetParam;
|
|
9
|
+
exports.resolveAccountScopes = resolveAccountScopes;
|
|
7
10
|
exports.normalizeOutboundTarget = normalizeOutboundTarget;
|
|
8
11
|
exports.resolveConfiguredAccountId = resolveConfiguredAccountId;
|
|
9
12
|
exports.inferOutboundTargetKind = inferOutboundTargetKind;
|
|
@@ -36,6 +39,7 @@ exports.prefixReplyTextToAddress = prefixReplyTextToAddress;
|
|
|
36
39
|
exports.resolveReplyTarget = resolveReplyTarget;
|
|
37
40
|
exports.resolveChatTarget = resolveChatTarget;
|
|
38
41
|
exports.isReplyToSelfMessage = isReplyToSelfMessage;
|
|
42
|
+
exports.resolveReplyParent = resolveReplyParent;
|
|
39
43
|
exports.resolveSenderProfile = resolveSenderProfile;
|
|
40
44
|
exports.resolveSenderProfileWithTimeout = resolveSenderProfileWithTimeout;
|
|
41
45
|
exports.normalizeParseMode = normalizeParseMode;
|
|
@@ -193,14 +197,74 @@ function readLatestAssistantFallbackFromTranscript(sessionKey, storePath, notBef
|
|
|
193
197
|
}
|
|
194
198
|
return undefined;
|
|
195
199
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The part of an inbound message the mention gate may read.
|
|
202
|
+
*
|
|
203
|
+
* A voice transcript is the sender's own speech: "Тина, посмотри" said aloud
|
|
204
|
+
* addresses the agent exactly as typing it would, and gating on the caption
|
|
205
|
+
* alone would make her deaf to being spoken to. A vision description is not
|
|
206
|
+
* speech — it is a model reading somebody else's content, so a screenshot of a
|
|
207
|
+
* chat in which a third party wrote `@tina_bot`, or a photo of a poster
|
|
208
|
+
* bearing the name, used to count as an address and wake her up.
|
|
209
|
+
*
|
|
210
|
+
* The body handed to the agent is unaffected; only what may count as being
|
|
211
|
+
* addressed is narrowed.
|
|
212
|
+
*/
|
|
213
|
+
function resolveAddressableText(input) {
|
|
214
|
+
if (input.understanding === "description") {
|
|
215
|
+
return input.messageText?.trim() ?? "";
|
|
200
216
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
return input.bodyText?.trim() ?? input.messageText?.trim() ?? "";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The chat named by a call, in the spellings this channel accepts.
|
|
221
|
+
*
|
|
222
|
+
* Five parsers spelled this chain out separately and none of them read
|
|
223
|
+
* `channelId` — which is the key core fills for its `channelId`-mode actions
|
|
224
|
+
* (`channel-info`, the name `chatInfo` answers to). A call that named another
|
|
225
|
+
* chat that way fell through to the current one and was answered about the
|
|
226
|
+
* wrong chat, silently. One list, one order, every caller.
|
|
227
|
+
*/
|
|
228
|
+
exports.CHAT_TARGET_PARAM_KEYS = ["chatId", "channelId", "target", "to", "chat"];
|
|
229
|
+
function readChatTargetParam(params, toolContext) {
|
|
230
|
+
// Strings only, exactly like the SDK's `readStringParam` and like every
|
|
231
|
+
// parser here before the chain was shared: a numeric `chatId` is refused
|
|
232
|
+
// rather than coerced, because `-1001234567890` loses precision as a JSON
|
|
233
|
+
// number long before it reaches Telegram.
|
|
234
|
+
const named = [];
|
|
235
|
+
for (const key of exports.CHAT_TARGET_PARAM_KEYS) {
|
|
236
|
+
const value = params?.[key];
|
|
237
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
238
|
+
if (trimmed && !named.includes(trimmed)) {
|
|
239
|
+
named.push(trimmed);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Two keys naming two different chats is a caller error, and picking one by
|
|
243
|
+
// key order would send a message to a chat nobody asked for — the failure
|
|
244
|
+
// this whole resolver exists to prevent. Say so instead of guessing.
|
|
245
|
+
if (named.length > 1) {
|
|
246
|
+
throw new Error(`clawgram: conflicting chat targets (${named.join(", ")})`);
|
|
247
|
+
}
|
|
248
|
+
return named[0] ?? toolContext?.currentChannelId?.trim() ?? "";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Which chat an outbound action is for.
|
|
252
|
+
*
|
|
253
|
+
* `chatId` and `channelId` are read alongside `to`/`target` because the read
|
|
254
|
+
* actions accept them and this plugin's own tool hints tell the agent to name
|
|
255
|
+
* a chat that way. Until 2.21.0 the send path ignored them: a `send` carrying
|
|
256
|
+
* `chatId` fell through to the current chat, so a message meant for another
|
|
257
|
+
* chat was delivered to the one the turn came from — the wrong audience, with
|
|
258
|
+
* no error anywhere.
|
|
259
|
+
*
|
|
260
|
+
* A named chat always wins over the context. The context fallback exists for
|
|
261
|
+
* a send that names nothing, which is the ordinary "answer where you were
|
|
262
|
+
* asked" case.
|
|
263
|
+
*/
|
|
264
|
+
function resolveActionTarget(params, toolContext) {
|
|
265
|
+
const target = readChatTargetParam(params, toolContext);
|
|
266
|
+
if (target) {
|
|
267
|
+
return target;
|
|
204
268
|
}
|
|
205
269
|
throw new Error("clawgram: message target is required");
|
|
206
270
|
}
|
|
@@ -258,19 +322,32 @@ function readMessageText(params) {
|
|
|
258
322
|
}
|
|
259
323
|
return "";
|
|
260
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Three states, and the empty one is the whole point:
|
|
327
|
+
*
|
|
328
|
+
* - absent (or a shape this cannot read) → `["*"]`, the historical default that
|
|
329
|
+
* keeps a fresh install usable;
|
|
330
|
+
* - `"*"` or a non-empty list → itself;
|
|
331
|
+
* - `[]`, `""`, or a list of blanks → `[]`, which denies everyone.
|
|
332
|
+
*
|
|
333
|
+
* The last used to return `["*"]`: an operator who emptied the list to shut the
|
|
334
|
+
* account off opened it to every Telegram user instead, and the sibling scopes
|
|
335
|
+
* (`readChats: []`, `manageChats: []`) already read an empty list as deny.
|
|
336
|
+
* `startAccount` logs a warning when this returns empty, because "nobody can
|
|
337
|
+
* reach the agent" must not be a silent state.
|
|
338
|
+
*/
|
|
261
339
|
function resolveAllowFrom(value) {
|
|
262
340
|
if (value === "*") {
|
|
263
341
|
return ["*"];
|
|
264
342
|
}
|
|
265
343
|
if (typeof value === "string" || typeof value === "number") {
|
|
266
344
|
const entry = String(value).trim();
|
|
267
|
-
return entry ? [entry] : [
|
|
345
|
+
return entry ? [entry] : [];
|
|
268
346
|
}
|
|
269
347
|
if (!Array.isArray(value)) {
|
|
270
348
|
return ["*"];
|
|
271
349
|
}
|
|
272
|
-
|
|
273
|
-
return entries.length > 0 ? entries : ["*"];
|
|
350
|
+
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
274
351
|
}
|
|
275
352
|
/**
|
|
276
353
|
* Three rungs, widest first: `open` wakes on every message, `mention` on the
|
|
@@ -306,6 +383,28 @@ function resolveGroupPromptSettings(groupConfig) {
|
|
|
306
383
|
}
|
|
307
384
|
return settings;
|
|
308
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* The sender scopes for one account: who may write, and what each group allows.
|
|
388
|
+
*
|
|
389
|
+
* `allowFrom` and `groups` are declared both per account and at the channel
|
|
390
|
+
* level, and the manifest validates them in both places — but only the account
|
|
391
|
+
* copies were read, so a channel-level allowlist passed validation and then
|
|
392
|
+
* admitted everyone. Worse, the config was read twice by two different paths
|
|
393
|
+
* (`resolveAccount` and the inbound handler), which is how they could disagree
|
|
394
|
+
* at all. One function serves both now.
|
|
395
|
+
*
|
|
396
|
+
* The account's own value wins, including a deliberate `[]`; the channel level
|
|
397
|
+
* is the default beneath it. Groups merge per key rather than replacing, so a
|
|
398
|
+
* channel-wide default and a per-account override can coexist.
|
|
399
|
+
*/
|
|
400
|
+
function resolveAccountScopes(cfg, accountId) {
|
|
401
|
+
const channel = cfg?.channels?.clawgram;
|
|
402
|
+
const account = channel?.accounts?.[accountId];
|
|
403
|
+
return {
|
|
404
|
+
allowFrom: resolveAllowFrom(account?.allowFrom ?? channel?.allowFrom),
|
|
405
|
+
groups: { ...resolveGroups(channel?.groups), ...resolveGroups(account?.groups) },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
309
408
|
function resolveGroups(value) {
|
|
310
409
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
311
410
|
return {};
|
|
@@ -549,27 +648,58 @@ async function resolveChatTarget(message) {
|
|
|
549
648
|
}
|
|
550
649
|
return message?.inputChat ?? message?._inputChat ?? message?.chat ?? message?._chat ?? message?.peerId;
|
|
551
650
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
651
|
+
/**
|
|
652
|
+
* The message a reply points at, fetched once.
|
|
653
|
+
*
|
|
654
|
+
* Telegram does not put the parent's text into the reply; a highlight
|
|
655
|
+
* (`quoteText`) is the only fragment that travels with it, and most replies
|
|
656
|
+
* have none. Core renders `[Replying to: …]` from the highlight or, failing
|
|
657
|
+
* that, from `ReplyToBody` — so without this fetch a plain reply reaches the
|
|
658
|
+
* agent as a bare parent id. The case that exposed it: the owner answered, in
|
|
659
|
+
* a DM, the agent's own notice about an unknown sender; the notice had been
|
|
660
|
+
* sent from another session, DMs are outside `readChats`, and the agent had
|
|
661
|
+
* no way to learn what "reply to #1011" referred to.
|
|
662
|
+
*
|
|
663
|
+
* Failure degrades to "no context" on purpose: a parent that cannot be
|
|
664
|
+
* fetched (deleted, flood-waited, transport hiccup) must not cost the
|
|
665
|
+
* message itself.
|
|
666
|
+
*/
|
|
667
|
+
async function resolveReplyParent(message, input) {
|
|
556
668
|
const replyToMessageId = message?.replyTo?.replyToMsgId ?? message?.replyToMsgId;
|
|
557
669
|
if (!replyToMessageId) {
|
|
558
|
-
return false;
|
|
670
|
+
return { isSelf: false };
|
|
559
671
|
}
|
|
560
672
|
const replied = typeof message?.getReplyMessage === "function"
|
|
561
673
|
? await message.getReplyMessage().catch(() => undefined)
|
|
562
674
|
: undefined;
|
|
563
675
|
if (!replied) {
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
if (replied.out === true) {
|
|
567
|
-
return true;
|
|
676
|
+
return { isSelf: false };
|
|
568
677
|
}
|
|
569
678
|
const replySenderId = replied.senderId ??
|
|
570
679
|
replied.fromId?.userId ??
|
|
571
680
|
replied.fromId?.channelId;
|
|
572
|
-
|
|
681
|
+
const isSelf = replied.out === true ||
|
|
682
|
+
(input.selfId !== undefined && replySenderId !== undefined && String(replySenderId) === input.selfId);
|
|
683
|
+
const rawText = typeof replied.message === "string" ? replied.message :
|
|
684
|
+
typeof replied.text === "string" ? replied.text :
|
|
685
|
+
"";
|
|
686
|
+
const body = rawText.trim() ? rawText : undefined;
|
|
687
|
+
const source = replied.sender ?? replied._sender;
|
|
688
|
+
const sender = isSelf
|
|
689
|
+
? (input.selfLabel ?? (input.selfId !== undefined ? input.selfId : undefined))
|
|
690
|
+
: toDisplayName({
|
|
691
|
+
firstName: typeof source?.firstName === "string" ? source.firstName : undefined,
|
|
692
|
+
lastName: typeof source?.lastName === "string" ? source.lastName : undefined,
|
|
693
|
+
username: undefined,
|
|
694
|
+
fallback: resolveActiveUsername(source) ? `@${resolveActiveUsername(source)}` : (replySenderId !== undefined ? String(replySenderId) : undefined),
|
|
695
|
+
});
|
|
696
|
+
return { isSelf, body, sender: sender || undefined };
|
|
697
|
+
}
|
|
698
|
+
async function isReplyToSelfMessage(message, selfId) {
|
|
699
|
+
if (!selfId) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
return (await resolveReplyParent(message, { selfId })).isSelf;
|
|
573
703
|
}
|
|
574
704
|
async function resolveSenderProfile(message, input) {
|
|
575
705
|
const pickProfile = (source) => {
|
package/dist/history.js
CHANGED
|
@@ -28,6 +28,7 @@ exports.HISTORY_DEFAULT_LIMIT = 100;
|
|
|
28
28
|
exports.HISTORY_MAX_LIMIT = 500;
|
|
29
29
|
const media_1 = require("./media");
|
|
30
30
|
const helpers_1 = require("./helpers");
|
|
31
|
+
const constants_1 = require("./constants");
|
|
31
32
|
/**
|
|
32
33
|
* Matches `normalize.ts` and `gramjs-client.ts` deliberately.
|
|
33
34
|
*
|
|
@@ -107,7 +108,7 @@ function parseMessageId(value, field) {
|
|
|
107
108
|
return numeric;
|
|
108
109
|
}
|
|
109
110
|
function parseListMessagesParams(params) {
|
|
110
|
-
const rawTarget =
|
|
111
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params);
|
|
111
112
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
112
113
|
if (!target) {
|
|
113
114
|
throw new Error("clawgram: list requires a chatId");
|
|
@@ -190,7 +191,7 @@ function parseParticipantsFilter(params) {
|
|
|
190
191
|
* not silently turn into an unbounded response.
|
|
191
192
|
*/
|
|
192
193
|
function parseListParticipantsParams(params) {
|
|
193
|
-
const rawTarget =
|
|
194
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params);
|
|
194
195
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
195
196
|
if (!target) {
|
|
196
197
|
throw new Error("clawgram: participants requires a chatId");
|
|
@@ -252,6 +253,14 @@ function normalizeChatKey(value) {
|
|
|
252
253
|
* rather than a sentence in a prompt that a model may be argued out of.
|
|
253
254
|
*/
|
|
254
255
|
function isChatReadable(target, readChats) {
|
|
256
|
+
// The inbound path has always dropped Telegram's service chat so login codes
|
|
257
|
+
// never reach the model; the read path did not, so `read chatId=777000`
|
|
258
|
+
// handed them over whenever `readChats` was absent (the default `--auth`
|
|
259
|
+
// writes) or held `*`. The deny is unconditional on purpose: no deployment
|
|
260
|
+
// has a reason to let the agent read its own login codes, and a config entry
|
|
261
|
+
// that enabled it would be an account-takeover switch.
|
|
262
|
+
if (normalizeChatKey(target) === constants_1.TELEGRAM_SERVICE_CHAT_ID)
|
|
263
|
+
return false;
|
|
255
264
|
if (readChats === undefined || readChats === null)
|
|
256
265
|
return true;
|
|
257
266
|
const entries = (Array.isArray(readChats) ? readChats : [readChats])
|
package/dist/manage.js
CHANGED
|
@@ -32,6 +32,7 @@ exports.resolveCreatedChannelId = resolveCreatedChannelId;
|
|
|
32
32
|
exports.summarizeMissingInvitees = summarizeMissingInvitees;
|
|
33
33
|
exports.readInviteLink = readInviteLink;
|
|
34
34
|
const history_1 = require("./history");
|
|
35
|
+
const helpers_1 = require("./helpers");
|
|
35
36
|
function readString(value) {
|
|
36
37
|
if (typeof value !== "string") {
|
|
37
38
|
return undefined;
|
|
@@ -47,7 +48,7 @@ function readUserRef(value) {
|
|
|
47
48
|
return readString(value);
|
|
48
49
|
}
|
|
49
50
|
function readTarget(params, toolContext, action) {
|
|
50
|
-
const raw =
|
|
51
|
+
const raw = (0, helpers_1.readChatTargetParam)(params, toolContext);
|
|
51
52
|
const target = readString(raw);
|
|
52
53
|
if (!target) {
|
|
53
54
|
throw new Error(`clawgram: ${action} requires a chatId`);
|
package/dist/media.js
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
* 240 KB" was posted is what a summary needs, and fetching the bytes of every
|
|
12
12
|
* attachment in a window would be a different feature with different costs.
|
|
13
13
|
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
14
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
18
|
exports.describeMedia = describeMedia;
|
|
16
19
|
exports.inboundMediaUnderstanding = inboundMediaUnderstanding;
|
|
17
20
|
exports.downloadInboundMediaToTempFile = downloadInboundMediaToTempFile;
|
|
18
21
|
exports.downloadMessageMediaToFile = downloadMessageMediaToFile;
|
|
19
22
|
exports.pruneFetchedMedia = pruneFetchedMedia;
|
|
23
|
+
exports.isLocalMediaPath = isLocalMediaPath;
|
|
24
|
+
exports.assertLocalMediaWithinRoots = assertLocalMediaWithinRoots;
|
|
25
|
+
const node_fs_1 = require("node:fs");
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
27
|
/**
|
|
21
28
|
* GramJS carries numbers as `big-integer` objects as often as native numbers —
|
|
22
29
|
* the same shape that once made `senderId` silently undefined. Anything that
|
|
@@ -237,3 +244,52 @@ function extensionFor(media, understanding) {
|
|
|
237
244
|
return "m4a";
|
|
238
245
|
return "ogg";
|
|
239
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Whether an outbound `file` is a local path rather than a URL.
|
|
249
|
+
*
|
|
250
|
+
* GramJS accepts both, and only a path can reach the host filesystem.
|
|
251
|
+
*/
|
|
252
|
+
function isLocalMediaPath(file) {
|
|
253
|
+
if (typeof file !== "string" || !file.trim()) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(file.trim());
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Refuses an outbound local file that sits outside the roots the host declared.
|
|
260
|
+
*
|
|
261
|
+
* Core hands every action the `mediaLocalRoots` the agent is scoped to, and
|
|
262
|
+
* bundled channels enforce them; this one passed `filePath` straight to
|
|
263
|
+
* `sendFile`, so a prompt-injected agent could name `/opt/openclaw-secrets/
|
|
264
|
+
* secrets.json` — or the config holding `sessionString` — and have it uploaded
|
|
265
|
+
* to any peer. Symlinks are resolved first, because a link inside a root
|
|
266
|
+
* pointing out of it is the obvious way around a prefix check.
|
|
267
|
+
*
|
|
268
|
+
* When the host declares no roots the path is left alone: the gateway RPC and
|
|
269
|
+
* the TTS contour both send files core never scoped, and refusing them here
|
|
270
|
+
* would break sending altogether rather than narrow it.
|
|
271
|
+
*/
|
|
272
|
+
function assertLocalMediaWithinRoots(file, roots) {
|
|
273
|
+
if (!isLocalMediaPath(file) || !roots || roots.length === 0) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const realPath = (candidate) => {
|
|
277
|
+
try {
|
|
278
|
+
return (0, node_fs_1.realpathSync)(candidate);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return node_path_1.default.resolve(candidate);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const target = realPath(file.trim());
|
|
285
|
+
const allowed = roots.some((root) => {
|
|
286
|
+
const resolvedRoot = realPath(root);
|
|
287
|
+
return target === resolvedRoot || target.startsWith(`${resolvedRoot}${node_path_1.default.sep}`);
|
|
288
|
+
});
|
|
289
|
+
if (!allowed) {
|
|
290
|
+
// The path is the agent's own input, not a secret, and naming it is what
|
|
291
|
+
// makes the refusal actionable. The roots are not listed: they describe
|
|
292
|
+
// the host's layout.
|
|
293
|
+
throw new Error(`clawgram: ${file} is outside the media roots this agent may read`);
|
|
294
|
+
}
|
|
295
|
+
}
|
package/dist/reactions.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.resolveAgentReactionGuidance = resolveAgentReactionGuidance;
|
|
15
15
|
exports.parseReactionParams = parseReactionParams;
|
|
16
|
+
const helpers_1 = require("./helpers");
|
|
16
17
|
/**
|
|
17
18
|
* How freely the agent may react, from the account's `reactionLevel`.
|
|
18
19
|
*
|
|
@@ -69,7 +70,7 @@ function parseMessageId(value) {
|
|
|
69
70
|
return parsed;
|
|
70
71
|
}
|
|
71
72
|
function parseReactionParams(params, toolContext) {
|
|
72
|
-
const rawTarget =
|
|
73
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params, toolContext);
|
|
73
74
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
74
75
|
if (!target) {
|
|
75
76
|
throw new Error("clawgram: react requires a chatId");
|
package/dist/topics.js
CHANGED
|
@@ -16,10 +16,11 @@ exports.normalizeForumTopics = normalizeForumTopics;
|
|
|
16
16
|
* client; the transport lives in `GramJsClientManager.listTopics`.
|
|
17
17
|
*/
|
|
18
18
|
const normalize_js_1 = require("./normalize.js");
|
|
19
|
+
const helpers_1 = require("./helpers");
|
|
19
20
|
exports.TOPICS_DEFAULT_LIMIT = 100;
|
|
20
21
|
exports.TOPICS_MAX_LIMIT = 500;
|
|
21
22
|
function parseTopicsParams(params) {
|
|
22
|
-
const rawTarget =
|
|
23
|
+
const rawTarget = (0, helpers_1.readChatTargetParam)(params);
|
|
23
24
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
24
25
|
if (!target) {
|
|
25
26
|
throw new Error("clawgram: topics requires a chatId");
|
package/dist/update-config.js
CHANGED
|
@@ -351,10 +351,26 @@ function buildUpdatedConfigText(raw, accountId, auth) {
|
|
|
351
351
|
}
|
|
352
352
|
return replaceObjectPropertyValue(raw, channelsProperty, updatedChannels, format);
|
|
353
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* The config this writes holds `apiHash` and `sessionString` in plaintext, so
|
|
356
|
+
* the permissions of the file it replaces are part of the credential's
|
|
357
|
+
* protection. `writeFile`'s own `mode` is masked by the process umask (0644
|
|
358
|
+
* under the usual 022), which is how a 0600 config came back world-readable
|
|
359
|
+
* after `--auth`; the explicit `chmod` on the temp file is what actually
|
|
360
|
+
* fixes it, and it happens before the rename so the config is never briefly
|
|
361
|
+
* readable at the wrong mode.
|
|
362
|
+
*/
|
|
363
|
+
const SECRET_FILE_MODE = 0o600;
|
|
364
|
+
async function readFileMode(filePath) {
|
|
365
|
+
const stats = await node_fs_1.promises.stat(filePath).catch(() => null);
|
|
366
|
+
return stats ? stats.mode & 0o777 : SECRET_FILE_MODE;
|
|
367
|
+
}
|
|
354
368
|
async function writeConfigAtomically(configPath, raw) {
|
|
355
369
|
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
370
|
+
const mode = await readFileMode(configPath);
|
|
356
371
|
try {
|
|
357
|
-
await node_fs_1.promises.writeFile(tempPath, raw, "utf8");
|
|
372
|
+
await node_fs_1.promises.writeFile(tempPath, raw, { encoding: "utf8", mode });
|
|
373
|
+
await node_fs_1.promises.chmod(tempPath, mode);
|
|
358
374
|
await node_fs_1.promises.rename(tempPath, configPath);
|
|
359
375
|
}
|
|
360
376
|
catch (error) {
|
|
@@ -367,8 +383,12 @@ async function createConfigBackup(configPath) {
|
|
|
367
383
|
if (raw === null) {
|
|
368
384
|
return null;
|
|
369
385
|
}
|
|
386
|
+
// The backup is a verbatim copy of a credential file and it is never cleaned
|
|
387
|
+
// up, so it inherits the original's mode rather than the umask default.
|
|
388
|
+
const mode = await readFileMode(configPath);
|
|
370
389
|
const backupPath = buildConfigBackupPath(configPath);
|
|
371
|
-
await node_fs_1.promises.writeFile(backupPath, raw, "utf8");
|
|
390
|
+
await node_fs_1.promises.writeFile(backupPath, raw, { encoding: "utf8", mode });
|
|
391
|
+
await node_fs_1.promises.chmod(backupPath, mode);
|
|
372
392
|
return backupPath;
|
|
373
393
|
}
|
|
374
394
|
async function updateConfigFileDirectly(configPath, accountId, auth) {
|
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.21.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"tag"
|
|
58
58
|
],
|
|
59
59
|
"default": "mention",
|
|
60
|
-
"description": "What wakes the agent in this chat (2.18.0). `mention`
|
|
60
|
+
"description": "What wakes the agent in this chat (2.18.0). `mention` — the name, an @mention or a reply to it; `open` — every message, the agent decides for itself whether words are owed; `tag` — only an explicit @username or a reply, for chats where the name occurs in conversation constantly."
|
|
61
61
|
},
|
|
62
62
|
"allowFrom": {
|
|
63
63
|
"type": "array",
|
|
@@ -364,7 +364,7 @@
|
|
|
364
364
|
"tag"
|
|
365
365
|
],
|
|
366
366
|
"default": "mention",
|
|
367
|
-
"description": "What wakes the agent in this chat (2.18.0). `mention`
|
|
367
|
+
"description": "What wakes the agent in this chat (2.18.0). `mention` — the name, an @mention or a reply to it; `open` — every message; `tag` — only an explicit @username or a reply."
|
|
368
368
|
},
|
|
369
369
|
"allowFrom": {
|
|
370
370
|
"type": "array",
|
package/package.json
CHANGED