clawgram 2.24.0 → 2.26.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 +40 -10
- package/dist/account-registry.js +33 -0
- package/dist/channel.js +47 -92
- package/dist/chunk.js +43 -0
- package/dist/gramjs-client.js +31 -26
- package/dist/helpers.js +5 -2
- package/dist/history.js +52 -5
- package/dist/inbound-pipeline.js +139 -112
- package/dist/manage.js +1 -6
- package/dist/media.js +22 -0
- package/dist/outbound.js +32 -21
- package/dist/send-scope.js +13 -26
- package/dist/system-notice.js +0 -20
- package/dist/update-config.js +0 -8
- package/npm-shrinkwrap.json +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -1
package/dist/inbound-pipeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.visibleReplyText = visibleReplyText;
|
|
3
4
|
exports.handleInboundEvent = handleInboundEvent;
|
|
4
5
|
// Входящий контур: одно событие Telegram от нормализации до ответа.
|
|
5
6
|
//
|
|
@@ -26,6 +27,7 @@ const normalize_1 = require("./normalize");
|
|
|
26
27
|
const reactions_1 = require("./reactions");
|
|
27
28
|
const silent_reaction_1 = require("./silent-reaction");
|
|
28
29
|
const system_notice_1 = require("./system-notice");
|
|
30
|
+
const account_registry_1 = require("./account-registry");
|
|
29
31
|
const group_reply_address_1 = require("./group-reply-address");
|
|
30
32
|
const helpers_1 = require("./helpers");
|
|
31
33
|
const constants_2 = require("./constants");
|
|
@@ -67,8 +69,53 @@ async function reactToSilentMentionForAccount(params) {
|
|
|
67
69
|
});
|
|
68
70
|
}
|
|
69
71
|
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
72
|
+
/**
|
|
73
|
+
* The text a reply may carry into a chat, after the filters every reply path
|
|
74
|
+
* shares — or `undefined` when nothing should go out.
|
|
75
|
+
*
|
|
76
|
+
* Three doors deliver an agent's words: the group `deliver` closure, the
|
|
77
|
+
* direct-message `deliver` closure and the transcript fallback. Each carried
|
|
78
|
+
* its own copy of the same two checks — drop the silent token, drop core's
|
|
79
|
+
* telemetry — and the copies drifted: the DM path had no notice filter at
|
|
80
|
+
* all until B5-01 (audit B5-13). One function now; the log lines keep their
|
|
81
|
+
* historical wording so journals stay greppable.
|
|
82
|
+
*/
|
|
83
|
+
function visibleReplyText(params) {
|
|
84
|
+
// Not destructured: a wiring ratchet in test/inbound-pipeline.test.ts finds
|
|
85
|
+
// handleInboundEvent's own destructuring of its context by pattern, and
|
|
86
|
+
// nothing shaped like it may stand in front.
|
|
87
|
+
const accountId = params.accountId;
|
|
88
|
+
const chatId = params.chatId;
|
|
89
|
+
const messageId = params.messageId;
|
|
90
|
+
const log = params.log;
|
|
91
|
+
const outboundText = typeof params.text === "string" ? params.text.trim() : "";
|
|
92
|
+
if (!outboundText) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
// The agent may decline to answer by returning the shared silent token.
|
|
96
|
+
// Dropped before addressing: otherwise the reply-address prefix turns it
|
|
97
|
+
// into a visible message.
|
|
98
|
+
const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
|
|
99
|
+
if (!visibleText) {
|
|
100
|
+
log?.info?.(`clawgram suppressing silent ${params.where}`, { accountId, chatId, messageId });
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
// Core glues its telemetry to the turn's payload and it arrives here the
|
|
104
|
+
// same way an answer does. In a group it never goes out; in a DM only the
|
|
105
|
+
// named operator may receive it (A5-10, A5-11, B5-01).
|
|
106
|
+
const notice = (0, system_notice_1.shouldSuppressGroupSystemNotice)(params.kind === "group"
|
|
107
|
+
? { targetKind: "group", text: visibleText }
|
|
108
|
+
: { targetKind: "user", text: visibleText, to: chatId, operatorIds: (0, account_registry_1.operatorIdsFor)(accountId) });
|
|
109
|
+
if (notice) {
|
|
110
|
+
log?.warn?.(`clawgram suppressing system notice in ${params.where}`, {
|
|
111
|
+
accountId, chatId, messageId, noticeKind: notice, textLength: visibleText.length,
|
|
112
|
+
});
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return visibleText;
|
|
116
|
+
}
|
|
70
117
|
async function handleInboundEvent(event, ctx) {
|
|
71
|
-
const { accountId, cfg, channelRuntime, client, gram, log,
|
|
118
|
+
const { accountId, cfg, channelRuntime, client, gram, log, pluginRuntime, runtimes, selfId, selfLabel, selfUsername } = ctx;
|
|
72
119
|
try {
|
|
73
120
|
const rawMessage = event?.message;
|
|
74
121
|
const rawPeerUserId = rawMessage?.peerId?.userId;
|
|
@@ -101,30 +148,6 @@ async function handleInboundEvent(event, ctx) {
|
|
|
101
148
|
}
|
|
102
149
|
return;
|
|
103
150
|
}
|
|
104
|
-
const directReplyTarget = normalized.chatType === "direct"
|
|
105
|
-
? undefined
|
|
106
|
-
: await (0, helpers_1.resolveReplyTarget)(rawMessage);
|
|
107
|
-
const senderProfile = normalized.chatType === "direct"
|
|
108
|
-
? await (0, helpers_1.resolveSenderProfileWithTimeout)(rawMessage, {
|
|
109
|
-
senderId: normalized.senderId,
|
|
110
|
-
client,
|
|
111
|
-
}, 1500)
|
|
112
|
-
: await (0, helpers_1.resolveSenderProfile)(rawMessage, {
|
|
113
|
-
senderId: normalized.senderId,
|
|
114
|
-
client,
|
|
115
|
-
});
|
|
116
|
-
const replyTarget = normalized.chatType === "direct"
|
|
117
|
-
? normalized.chatId
|
|
118
|
-
: await (0, helpers_1.resolveChatTarget)(rawMessage);
|
|
119
|
-
if (replyTarget) {
|
|
120
|
-
normalized.replyTarget = replyTarget;
|
|
121
|
-
}
|
|
122
|
-
if (!normalized.senderUsername && senderProfile.username) {
|
|
123
|
-
normalized.senderUsername = senderProfile.username;
|
|
124
|
-
}
|
|
125
|
-
if (!normalized.senderDisplay && senderProfile.display) {
|
|
126
|
-
normalized.senderDisplay = senderProfile.display;
|
|
127
|
-
}
|
|
128
151
|
if (normalized.isOutgoing) {
|
|
129
152
|
if (normalized.chatType === "direct") {
|
|
130
153
|
log?.info?.("clawgram skipping outgoing direct event", {
|
|
@@ -145,6 +168,61 @@ async function handleInboundEvent(event, ctx) {
|
|
|
145
168
|
});
|
|
146
169
|
return;
|
|
147
170
|
}
|
|
171
|
+
// Ворота — ДО сети. До 2.25.0 на каждое сообщение из любой группы,
|
|
172
|
+
// где сидит аккаунт, — включая группы вне `groups` — плагин делал
|
|
173
|
+
// до семи запросов к Telegram (профиль отправителя, адрес ответа,
|
|
174
|
+
// цель чата) и только потом отбрасывал сообщение как чужое.
|
|
175
|
+
// Посторонний, флудящий в такой группе, тратил соединение и
|
|
176
|
+
// rate-limit аккаунта (B5-04, остаток A5-06). Группа вне конфига
|
|
177
|
+
// и выключенная группа заканчиваются здесь, без единого вызова.
|
|
178
|
+
const earlyScopes = (0, helpers_1.resolveAccountScopes)(cfg, accountId);
|
|
179
|
+
const earlyGroupConfig = normalized.chatType === "group"
|
|
180
|
+
? (0, helpers_1.resolveGroupConfig)(earlyScopes.groups, normalized.chatId)
|
|
181
|
+
: undefined;
|
|
182
|
+
if (normalized.chatType === "group") {
|
|
183
|
+
if (!earlyGroupConfig) {
|
|
184
|
+
log?.info?.("clawgram skipping group not present in groups config", {
|
|
185
|
+
accountId,
|
|
186
|
+
chatId: normalized.chatId,
|
|
187
|
+
messageId: normalized.messageId,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (earlyGroupConfig.enabled === false) {
|
|
192
|
+
log?.info?.("clawgram skipping disabled group", {
|
|
193
|
+
accountId,
|
|
194
|
+
chatId: normalized.chatId,
|
|
195
|
+
messageId: normalized.messageId,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Профиль отправителя нужен воротам только когда allowFrom
|
|
201
|
+
// называет кого-то по @handle, а сообщение handle не принесло;
|
|
202
|
+
// числовые id сверяются без сети.
|
|
203
|
+
const gateAllowFrom = normalized.chatType === "group"
|
|
204
|
+
? earlyGroupConfig?.allowFrom
|
|
205
|
+
: earlyScopes.allowFrom;
|
|
206
|
+
const allowFromNeedsHandle = Array.isArray(gateAllowFrom)
|
|
207
|
+
&& gateAllowFrom.some((entry) => String(entry).trim().startsWith("@"));
|
|
208
|
+
const needsProfile = !normalized.senderUsername && allowFromNeedsHandle;
|
|
209
|
+
const senderProfile = needsProfile
|
|
210
|
+
? (normalized.chatType === "direct"
|
|
211
|
+
? await (0, helpers_1.resolveSenderProfileWithTimeout)(rawMessage, {
|
|
212
|
+
senderId: normalized.senderId,
|
|
213
|
+
client,
|
|
214
|
+
}, 1500)
|
|
215
|
+
: await (0, helpers_1.resolveSenderProfile)(rawMessage, {
|
|
216
|
+
senderId: normalized.senderId,
|
|
217
|
+
client,
|
|
218
|
+
}))
|
|
219
|
+
: {};
|
|
220
|
+
if (!normalized.senderUsername && senderProfile.username) {
|
|
221
|
+
normalized.senderUsername = senderProfile.username;
|
|
222
|
+
}
|
|
223
|
+
if (!normalized.senderDisplay && senderProfile.display) {
|
|
224
|
+
normalized.senderDisplay = senderProfile.display;
|
|
225
|
+
}
|
|
148
226
|
let text = normalized.text?.trim();
|
|
149
227
|
// Whether this sender may reach the agent at all — decided before
|
|
150
228
|
// the attachment is fetched.
|
|
@@ -180,6 +258,16 @@ async function handleInboundEvent(event, ctx) {
|
|
|
180
258
|
// voice note and a screenshot alike, the attachment *is* the
|
|
181
259
|
// message. A caption is kept and the reading appended, because
|
|
182
260
|
// "look at this" plus the picture is one thought, not two.
|
|
261
|
+
// Адрес ответа и цель чата — сеть, и нужны только тому, кому
|
|
262
|
+
// отвечают: считаются после ворот (B5-04).
|
|
263
|
+
const directReplyTarget = normalized.chatType === "direct" ? undefined
|
|
264
|
+
: senderMayReachAgent ? await (0, helpers_1.resolveReplyTarget)(rawMessage) : undefined;
|
|
265
|
+
const replyTarget = normalized.chatType === "direct"
|
|
266
|
+
? normalized.chatId
|
|
267
|
+
: senderMayReachAgent ? await (0, helpers_1.resolveChatTarget)(rawMessage) : undefined;
|
|
268
|
+
if (replyTarget) {
|
|
269
|
+
normalized.replyTarget = replyTarget;
|
|
270
|
+
}
|
|
183
271
|
const attachment = senderMayReachAgent ? await (0, attachments_1.readInboundAttachment)({
|
|
184
272
|
gram,
|
|
185
273
|
event,
|
|
@@ -285,6 +373,12 @@ async function handleInboundEvent(event, ctx) {
|
|
|
285
373
|
// the answer — and by the same resolver `resolveAccount` uses, so the
|
|
286
374
|
// gate applied here is the one the account was started with.
|
|
287
375
|
const { allowFrom: directAllowFrom } = inboundScopes;
|
|
376
|
+
// Fixed, not configurable: clawgram admits a DM by `allowFrom` alone
|
|
377
|
+
// (the roster) and offers no pairing challenge — a stranger cannot
|
|
378
|
+
// talk their way in. Core's resolver is still called for the block
|
|
379
|
+
// decision and `commandAuthorized`; with "open" it never answers
|
|
380
|
+
// "pairing", so the 30-line challenge branch that once followed it
|
|
381
|
+
// was unreachable and read like a barrier (audit B5-15).
|
|
288
382
|
const dmPolicy = "open";
|
|
289
383
|
if (normalized.chatType === "group") {
|
|
290
384
|
const groupConfig = inboundGroupConfig;
|
|
@@ -315,7 +409,10 @@ async function handleInboundEvent(event, ctx) {
|
|
|
315
409
|
messageId: normalized.messageId,
|
|
316
410
|
senderId,
|
|
317
411
|
username: normalized.senderUsername,
|
|
318
|
-
|
|
412
|
+
// Сам список — id владельца и допущенных — в журнал не идёт:
|
|
413
|
+
// посторонний управлял бы числом его копий в journald (B5-09).
|
|
414
|
+
allowFromCount: Array.isArray(groupConfig.allowFrom) ? groupConfig.allowFrom.length : 0,
|
|
415
|
+
allowFromHasWildcard: Array.isArray(groupConfig.allowFrom) && groupConfig.allowFrom.some((e) => String(e).trim() === "*"),
|
|
319
416
|
});
|
|
320
417
|
return;
|
|
321
418
|
}
|
|
@@ -511,38 +608,11 @@ async function handleInboundEvent(event, ctx) {
|
|
|
511
608
|
payloadTextLength: outboundText.length,
|
|
512
609
|
payloadReplyToId: payload.replyToId ?? null,
|
|
513
610
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// The agent may decline to answer by returning the shared
|
|
518
|
-
// silent token. Drop it before addressing: otherwise the
|
|
519
|
-
// reply-address prefix turns it into a visible message.
|
|
520
|
-
const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
|
|
521
|
-
if (!visibleText) {
|
|
522
|
-
log?.info?.("clawgram suppressing silent group reply", {
|
|
523
|
-
accountId,
|
|
524
|
-
chatId: normalized.chatId,
|
|
525
|
-
messageId: normalized.messageId,
|
|
526
|
-
});
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
// Ядро подклеивает свою телеметрию к полезной нагрузке
|
|
530
|
-
// хода, и сюда она приходит тем же путём, что ответ.
|
|
531
|
-
// Проверка стояла только в `outbound.sendText`, то есть
|
|
532
|
-
// класс инцидента 30.08–01.09 был закрыт для рассылок и
|
|
533
|
-
// открыт для обычного ответа на упоминание (A5-10).
|
|
534
|
-
const groupNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
|
|
535
|
-
targetKind: "group",
|
|
536
|
-
text: visibleText,
|
|
611
|
+
const visibleText = visibleReplyText({
|
|
612
|
+
text: outboundText, kind: "group", where: "group reply",
|
|
613
|
+
accountId, chatId: normalized.chatId, messageId: normalized.messageId, log,
|
|
537
614
|
});
|
|
538
|
-
if (
|
|
539
|
-
log?.warn?.("clawgram suppressing system notice in group reply", {
|
|
540
|
-
accountId,
|
|
541
|
-
chatId: normalized.chatId,
|
|
542
|
-
messageId: normalized.messageId,
|
|
543
|
-
noticeKind: groupNotice,
|
|
544
|
-
textLength: visibleText.length,
|
|
545
|
-
});
|
|
615
|
+
if (!visibleText) {
|
|
546
616
|
return;
|
|
547
617
|
}
|
|
548
618
|
const replyToMessageId = payload.replyToId ? Number(payload.replyToId) : Number(normalized.messageId);
|
|
@@ -603,19 +673,10 @@ async function handleInboundEvent(event, ctx) {
|
|
|
603
673
|
: "";
|
|
604
674
|
// Тот же фильтр и здесь: последняя реплика в стенограмме
|
|
605
675
|
// вполне может оказаться именно уведомлением об ошибке.
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
:
|
|
609
|
-
|
|
610
|
-
log?.warn?.("clawgram suppressing system notice in transcript fallback", {
|
|
611
|
-
accountId,
|
|
612
|
-
chatId: normalized.chatId,
|
|
613
|
-
messageId: normalized.messageId,
|
|
614
|
-
noticeKind: fallbackNotice,
|
|
615
|
-
textLength: rawFallback.length,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
const visibleFallbackText = fallbackNotice ? "" : rawFallback;
|
|
676
|
+
const visibleFallbackText = visibleReplyText({
|
|
677
|
+
text: rawFallback, kind: "group", where: "transcript fallback",
|
|
678
|
+
accountId, chatId: normalized.chatId, messageId: normalized.messageId, log,
|
|
679
|
+
}) ?? "";
|
|
619
680
|
if (!visibleFallbackText) {
|
|
620
681
|
if (fallbackText) {
|
|
621
682
|
log?.info?.("clawgram skipping silent transcript fallback", {
|
|
@@ -706,7 +767,8 @@ async function handleInboundEvent(event, ctx) {
|
|
|
706
767
|
accountId,
|
|
707
768
|
senderId,
|
|
708
769
|
senderUsername: normalized.senderUsername,
|
|
709
|
-
|
|
770
|
+
allowFromCount: Array.isArray(directAllowFrom) ? directAllowFrom.length : 0,
|
|
771
|
+
allowFromHasWildcard: Array.isArray(directAllowFrom) && directAllowFrom.some((e) => String(e).trim() === "*"),
|
|
710
772
|
});
|
|
711
773
|
return;
|
|
712
774
|
}
|
|
@@ -724,7 +786,6 @@ async function handleInboundEvent(event, ctx) {
|
|
|
724
786
|
senderId,
|
|
725
787
|
senderUsername,
|
|
726
788
|
}),
|
|
727
|
-
readStoreAllowFrom: pairing.readStoreForDmPolicy,
|
|
728
789
|
});
|
|
729
790
|
if (access.access.decision === "block") {
|
|
730
791
|
log?.info?.("clawgram blocking inbound direct message", {
|
|
@@ -737,36 +798,6 @@ async function handleInboundEvent(event, ctx) {
|
|
|
737
798
|
});
|
|
738
799
|
return;
|
|
739
800
|
}
|
|
740
|
-
if (access.access.decision === "pairing") {
|
|
741
|
-
await pairing.issueChallenge({
|
|
742
|
-
senderId,
|
|
743
|
-
senderIdLine: `Your Telegram user id: ${senderId}`,
|
|
744
|
-
meta: {
|
|
745
|
-
username: normalized.senderUsername,
|
|
746
|
-
name: normalized.senderDisplay,
|
|
747
|
-
},
|
|
748
|
-
sendPairingReply: async (pairingText) => {
|
|
749
|
-
await sendTextToConversation({
|
|
750
|
-
text: pairingText,
|
|
751
|
-
});
|
|
752
|
-
},
|
|
753
|
-
onReplyError: (err) => {
|
|
754
|
-
log?.info?.("clawgram pairing reply failed", {
|
|
755
|
-
accountId,
|
|
756
|
-
chatId: normalized.chatId,
|
|
757
|
-
senderId,
|
|
758
|
-
error: String(err),
|
|
759
|
-
});
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
log?.info?.("clawgram pairing required for inbound direct message", {
|
|
763
|
-
accountId,
|
|
764
|
-
chatId: normalized.chatId,
|
|
765
|
-
messageId: normalized.messageId,
|
|
766
|
-
senderId,
|
|
767
|
-
});
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
801
|
// Same fetch as the group path. In a DM the parent is as often
|
|
771
802
|
// the agent's own message as the person's — the owner answers a
|
|
772
803
|
// notice she sent — and neither text is available any other way.
|
|
@@ -807,17 +838,13 @@ async function handleInboundEvent(event, ctx) {
|
|
|
807
838
|
NativeChannelId: normalized.chatId,
|
|
808
839
|
},
|
|
809
840
|
deliver: async (payload) => {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
841
|
+
// Тот же фильтр, что у группового ответа: личный ответ идёт
|
|
842
|
+
// третьим путём, и закрытие A5-11 его не покрывало (B5-01).
|
|
843
|
+
const visibleText = visibleReplyText({
|
|
844
|
+
text: payload.text, kind: "user", where: "direct reply",
|
|
845
|
+
accountId, chatId: normalized.chatId, messageId: normalized.messageId, log,
|
|
846
|
+
});
|
|
815
847
|
if (!visibleText) {
|
|
816
|
-
log?.info?.("clawgram suppressing silent direct reply", {
|
|
817
|
-
accountId,
|
|
818
|
-
chatId: normalized.chatId,
|
|
819
|
-
messageId: normalized.messageId,
|
|
820
|
-
});
|
|
821
848
|
return;
|
|
822
849
|
}
|
|
823
850
|
await sendTextToConversation({
|
package/dist/manage.js
CHANGED
|
@@ -202,12 +202,7 @@ function parseInviteLinkParams(params, toolContext) {
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
function normalizeScope(manageChats) {
|
|
205
|
-
|
|
206
|
-
return [];
|
|
207
|
-
}
|
|
208
|
-
return (Array.isArray(manageChats) ? manageChats : [manageChats])
|
|
209
|
-
.map(history_1.normalizeChatKey)
|
|
210
|
-
.filter(Boolean);
|
|
205
|
+
return (0, history_1.normalizeScopeList)(manageChats) ?? [];
|
|
211
206
|
}
|
|
212
207
|
/** True while the account is allowed to manage anything at all. */
|
|
213
208
|
function isManagementEnabled(manageChats) {
|
package/dist/media.js
CHANGED
|
@@ -22,6 +22,7 @@ exports.downloadMessageMediaToFile = downloadMessageMediaToFile;
|
|
|
22
22
|
exports.pruneFetchedMedia = pruneFetchedMedia;
|
|
23
23
|
exports.isLocalMediaPath = isLocalMediaPath;
|
|
24
24
|
exports.assertLocalMediaWithinRoots = assertLocalMediaWithinRoots;
|
|
25
|
+
exports.loadOutboundMedia = loadOutboundMedia;
|
|
25
26
|
const node_fs_1 = require("node:fs");
|
|
26
27
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
28
|
const util_1 = require("./util");
|
|
@@ -292,3 +293,24 @@ function assertLocalMediaWithinRoots(file, roots) {
|
|
|
292
293
|
throw new Error(`clawgram: ${file} is outside the media roots this agent may read`);
|
|
293
294
|
}
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The file an outbound send should hand to GramJS.
|
|
298
|
+
*
|
|
299
|
+
* Core scopes a call in two ways: `mediaLocalRoots` names the directories the
|
|
300
|
+
* agent may read from, and `mediaReadFile` is a reader that enforces them
|
|
301
|
+
* inside core. Bundled channels read local files through that reader; this
|
|
302
|
+
* one opened the path itself as the gateway process — the roots were checked
|
|
303
|
+
* here, the reader ignored, so a call core scoped with a reader and no roots
|
|
304
|
+
* (the default on the RPC and TTS paths) was not scoped at all (audit B5-14).
|
|
305
|
+
*
|
|
306
|
+
* Roots are still checked first, symlinks resolved. A local path is then read
|
|
307
|
+
* through core's reader when one is given, and sent as bytes under the file's
|
|
308
|
+
* own name; without a reader, or for a URL, the file goes to GramJS as before.
|
|
309
|
+
*/
|
|
310
|
+
async function loadOutboundMedia(file, roots, readFile) {
|
|
311
|
+
assertLocalMediaWithinRoots(file, roots);
|
|
312
|
+
if (!readFile || !isLocalMediaPath(file)) {
|
|
313
|
+
return file;
|
|
314
|
+
}
|
|
315
|
+
return { buffer: await readFile(file), fileName: node_path_1.default.basename(file) };
|
|
316
|
+
}
|
package/dist/outbound.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.createOutbound = createOutbound;
|
|
|
10
10
|
const core_1 = require("openclaw/plugin-sdk/core");
|
|
11
11
|
const media_1 = require("./media");
|
|
12
12
|
const send_scope_1 = require("./send-scope");
|
|
13
|
+
const account_registry_1 = require("./account-registry");
|
|
13
14
|
const system_notice_1 = require("./system-notice");
|
|
14
15
|
const group_reply_address_1 = require("./group-reply-address");
|
|
15
16
|
const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
|
|
@@ -50,16 +51,14 @@ function createOutbound(runtimes) {
|
|
|
50
51
|
// анонсы субагентов) идёт этим путём и мимо той проверки. Отказ
|
|
51
52
|
// здесь возвращается результатом, а не броском: бросок в этом хуке
|
|
52
53
|
// роняет весь gateway (грабли 06.08.2026, выше).
|
|
53
|
-
if (!(0, send_scope_1.isChatSendable)(target, (0,
|
|
54
|
-
const
|
|
55
|
-
? "phone-number target"
|
|
56
|
-
: "chat outside send scope";
|
|
54
|
+
if (!(0, send_scope_1.isChatSendable)(target, (0, account_registry_1.sendScopeFor)(ctx.accountId))) {
|
|
55
|
+
const refusal = (0, send_scope_1.describeSendRefusal)(target);
|
|
57
56
|
actionLog.warn("clawgram outbound resolveTarget refused", {
|
|
58
57
|
accountId: ctx.accountId,
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
reason: refusal.reason,
|
|
59
|
+
...refusal.logFields,
|
|
61
60
|
});
|
|
62
|
-
return { ok: false, error:
|
|
61
|
+
return { ok: false, error: refusal.error };
|
|
63
62
|
}
|
|
64
63
|
return { ok: true, to: target };
|
|
65
64
|
}
|
|
@@ -89,6 +88,20 @@ function createOutbound(runtimes) {
|
|
|
89
88
|
});
|
|
90
89
|
return { skipped: "silent" };
|
|
91
90
|
}
|
|
91
|
+
// Область отправки — и здесь. Путь доставки ядра (`--deliver`,
|
|
92
|
+
// анонсы субагентов) зовёт sendText напрямую, минуя resolveTarget и
|
|
93
|
+
// handleAction, где барьер уже стоял: третий из трёх исходящих путей
|
|
94
|
+
// был открыт для любого адресата и телефонного номера (D2-01, A5-12).
|
|
95
|
+
const scopedTarget = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
96
|
+
if (!(0, send_scope_1.isChatSendable)(scopedTarget, (0, account_registry_1.sendScopeFor)(ctx.accountId))) {
|
|
97
|
+
const refusal = (0, send_scope_1.describeSendRefusal)(scopedTarget);
|
|
98
|
+
actionLog.warn("clawgram outbound sendText refused", {
|
|
99
|
+
accountId: ctx.accountId,
|
|
100
|
+
reason: refusal.reason,
|
|
101
|
+
...refusal.logFields,
|
|
102
|
+
});
|
|
103
|
+
return { skipped: "not-allowed" };
|
|
104
|
+
}
|
|
92
105
|
// Core's operational chatter (tool-error warnings, fallback notices)
|
|
93
106
|
// stays out of group chats: it is telemetry for the operator, not a
|
|
94
107
|
// reply to the room, and it has already been seen carrying shell
|
|
@@ -98,7 +111,7 @@ function createOutbound(runtimes) {
|
|
|
98
111
|
targetKind: (0, helpers_1.inferOutboundTargetKind)(ctx.to),
|
|
99
112
|
text: ctx.text,
|
|
100
113
|
to: ctx.to,
|
|
101
|
-
operatorIds: (0,
|
|
114
|
+
operatorIds: (0, account_registry_1.operatorIdsFor)(ctx.accountId),
|
|
102
115
|
});
|
|
103
116
|
if (suppressedNotice) {
|
|
104
117
|
actionLog.warn("clawgram suppressing system notice in group", {
|
|
@@ -109,10 +122,7 @@ function createOutbound(runtimes) {
|
|
|
109
122
|
});
|
|
110
123
|
return { skipped: "system-notice" };
|
|
111
124
|
}
|
|
112
|
-
const gram =
|
|
113
|
-
if (!gram) {
|
|
114
|
-
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
115
|
-
}
|
|
125
|
+
const gram = (0, account_registry_1.requireRuntime)(runtimes, ctx.accountId);
|
|
116
126
|
// The agent already answered this message with its own `send`, and this
|
|
117
127
|
// is core delivering the same turn's final text. Two messages for one
|
|
118
128
|
// answer is how 2026-08-10 read in a work chat: every request reported
|
|
@@ -164,10 +174,7 @@ function createOutbound(runtimes) {
|
|
|
164
174
|
};
|
|
165
175
|
},
|
|
166
176
|
async sendMedia(ctx) {
|
|
167
|
-
const gram =
|
|
168
|
-
if (!gram) {
|
|
169
|
-
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
170
|
-
}
|
|
177
|
+
const gram = (0, account_registry_1.requireRuntime)(runtimes, ctx.accountId);
|
|
171
178
|
// Same rule as the action path: a local file outside the declared
|
|
172
179
|
// roots is refused before anything is uploaded.
|
|
173
180
|
const outboundRoots = ctx.mediaLocalRoots ?? ctx.mediaAccess?.localRoots;
|
|
@@ -184,8 +191,8 @@ function createOutbound(runtimes) {
|
|
|
184
191
|
hasCaption: Boolean(ctx.caption),
|
|
185
192
|
asVoice: ctx.audioAsVoice === true,
|
|
186
193
|
});
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
194
|
+
const named = ctx.filePath ?? ctx.mediaUrl;
|
|
195
|
+
if (!named) {
|
|
189
196
|
throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
|
|
190
197
|
}
|
|
191
198
|
// Ниже — проверки, которые у `sendText` были, а здесь не было ни
|
|
@@ -194,11 +201,12 @@ function createOutbound(runtimes) {
|
|
|
194
201
|
const mediaTarget = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
195
202
|
// Область отправки: файл наружу — такое же исходящее, как текст.
|
|
196
203
|
// `resolveTarget` ядро зовёт не на каждом пути, поэтому проверяем и тут.
|
|
197
|
-
if (!(0, send_scope_1.isChatSendable)(mediaTarget, (0,
|
|
204
|
+
if (!(0, send_scope_1.isChatSendable)(mediaTarget, (0, account_registry_1.sendScopeFor)(ctx.accountId))) {
|
|
205
|
+
const refusal = (0, send_scope_1.describeSendRefusal)(mediaTarget);
|
|
198
206
|
actionLog.warn("clawgram outbound sendMedia refused", {
|
|
199
207
|
accountId: ctx.accountId,
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
reason: refusal.reason,
|
|
209
|
+
...refusal.logFields,
|
|
202
210
|
});
|
|
203
211
|
return { skipped: "not-allowed" };
|
|
204
212
|
}
|
|
@@ -231,6 +239,9 @@ function createOutbound(runtimes) {
|
|
|
231
239
|
// exactly how a synthesized group reply died on 2026-08-08, silently
|
|
232
240
|
// enough that the transcript fallback posted it as raw text instead.
|
|
233
241
|
const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
242
|
+
// Read last, through core's scoped reader when it gave one: every
|
|
243
|
+
// refusal above must have passed before the file is opened.
|
|
244
|
+
const file = await (0, media_1.loadOutboundMedia)(named, outboundRoots, ctx.mediaReadFile ?? ctx.mediaAccess?.readFile);
|
|
234
245
|
const sent = await gram.sendMedia({
|
|
235
246
|
target,
|
|
236
247
|
file,
|
package/dist/send-scope.js
CHANGED
|
@@ -3,9 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isPhoneNumberTarget = isPhoneNumberTarget;
|
|
4
4
|
exports.isSendScopeConfigured = isSendScopeConfigured;
|
|
5
5
|
exports.isChatSendable = isChatSendable;
|
|
6
|
-
exports.
|
|
7
|
-
exports.sendScopeFor = sendScopeFor;
|
|
8
|
-
exports.forgetSendScope = forgetSendScope;
|
|
6
|
+
exports.describeSendRefusal = describeSendRefusal;
|
|
9
7
|
const history_1 = require("./history");
|
|
10
8
|
/**
|
|
11
9
|
* Outbound scope for the account: who this account may write to.
|
|
@@ -49,11 +47,7 @@ function isPhoneNumberTarget(target) {
|
|
|
49
47
|
return /[\s()\-.]/.test(raw) && /^\+?\d[\d\s()\-.]{5,}$/.test(raw);
|
|
50
48
|
}
|
|
51
49
|
function normalizeScope(sendChats) {
|
|
52
|
-
|
|
53
|
-
return [];
|
|
54
|
-
return (Array.isArray(sendChats) ? sendChats : [sendChats])
|
|
55
|
-
.map(history_1.normalizeChatKey)
|
|
56
|
-
.filter(Boolean);
|
|
50
|
+
return (0, history_1.normalizeScopeList)(sendChats) ?? [];
|
|
57
51
|
}
|
|
58
52
|
/** True while the account has a declared outbound scope at all. */
|
|
59
53
|
function isSendScopeConfigured(sendChats) {
|
|
@@ -74,24 +68,17 @@ function isChatSendable(target, sendChats) {
|
|
|
74
68
|
return (0, history_1.chatKeyCandidates)(target).some((candidate) => entries.includes(candidate));
|
|
75
69
|
}
|
|
76
70
|
/**
|
|
77
|
-
*
|
|
71
|
+
* One refusal for every outbound door — `handleAction`, `outbound.resolveTarget`,
|
|
72
|
+
* `sendText`, `sendMedia` — so the four read the same and log the same.
|
|
78
73
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* контракт ядра ради одной проверки. Тот же приём уже применён к списку
|
|
82
|
-
* операторов (`system-notice.ts`), и по той же причине.
|
|
83
|
-
*
|
|
84
|
-
* Перезапуск канала при правке конфига обновляет запись; аккаунт, о котором
|
|
85
|
-
* ничего не помним, ведёт себя как аккаунт без области — то есть отправка
|
|
86
|
-
* разрешена, но телефонный адресат всё равно отвергнут.
|
|
74
|
+
* A phone number is personal data: the journal gets the kind of target, not
|
|
75
|
+
* the value (B5-09). Two of the four doors used to log it anyway (D2-11).
|
|
87
76
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
function forgetSendScope(accountId) {
|
|
96
|
-
sendScopeByAccount.delete(accountId);
|
|
77
|
+
function describeSendRefusal(target) {
|
|
78
|
+
const phone = isPhoneNumberTarget(target);
|
|
79
|
+
return {
|
|
80
|
+
reason: phone ? "phone-number target" : "chat outside send scope",
|
|
81
|
+
logFields: phone ? { targetKind: "phone" } : { target },
|
|
82
|
+
error: new Error(`clawgram: not-allowed-chat ${target}`),
|
|
83
|
+
};
|
|
97
84
|
}
|
package/dist/system-notice.js
CHANGED
|
@@ -28,9 +28,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
28
28
|
exports.classifySystemNotice = classifySystemNotice;
|
|
29
29
|
exports.shouldSuppressGroupSystemNotice = shouldSuppressGroupSystemNotice;
|
|
30
30
|
exports.isOperatorRecipient = isOperatorRecipient;
|
|
31
|
-
exports.rememberOperatorIds = rememberOperatorIds;
|
|
32
|
-
exports.operatorIdsFor = operatorIdsFor;
|
|
33
|
-
exports.forgetOperatorIds = forgetOperatorIds;
|
|
34
31
|
const TOOL_WARNING_PREFIX = "⚠️ 🛠️ ";
|
|
35
32
|
const MESSAGE_FAILED_PREFIX = "⚠️ ✉️ message failed";
|
|
36
33
|
const FALLBACK_NOTICE_PREFIX = "↪️ model fallback";
|
|
@@ -96,20 +93,3 @@ function isOperatorRecipient(to, operatorIds) {
|
|
|
96
93
|
const target = String(to).trim().replace(/^@/, "").toLowerCase();
|
|
97
94
|
return operatorIds.some((id) => String(id).trim().replace(/^@/, "").toLowerCase() === target);
|
|
98
95
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Кто оператор у каждого аккаунта.
|
|
101
|
-
*
|
|
102
|
-
* Список запоминается при старте аккаунта: в `outbound.sendText` конфига нет,
|
|
103
|
-
* а тащить её туда параметром значило бы менять контракт ради одной проверки.
|
|
104
|
-
* Перезапуск канала при правке конфига обновляет запись.
|
|
105
|
-
*/
|
|
106
|
-
const operatorIdsByAccount = new Map();
|
|
107
|
-
function rememberOperatorIds(accountId, ids) {
|
|
108
|
-
operatorIdsByAccount.set(accountId, [...ids]);
|
|
109
|
-
}
|
|
110
|
-
function operatorIdsFor(accountId) {
|
|
111
|
-
return operatorIdsByAccount.get(accountId) ?? [];
|
|
112
|
-
}
|
|
113
|
-
function forgetOperatorIds(accountId) {
|
|
114
|
-
operatorIdsByAccount.delete(accountId);
|
|
115
|
-
}
|
package/dist/update-config.js
CHANGED
|
@@ -36,14 +36,6 @@ function buildAccountPayload(auth) {
|
|
|
36
36
|
sessionString: auth.sessionString,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
/** Первый конфиг закрыт: см. одноимённую функцию в cli-core.ts. */
|
|
40
|
-
function buildAccountConfigFragment(auth) {
|
|
41
|
-
return {
|
|
42
|
-
...buildAccountPayload(auth),
|
|
43
|
-
allowFrom: auth.selfId ? [auth.selfId] : [],
|
|
44
|
-
readChats: [],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
39
|
/**
|
|
48
40
|
* Credentials already moved into the secret store must survive re-auth.
|
|
49
41
|
*
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawgram",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clawgram",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.26.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"json5": "2.2.3",
|
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.26.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|