clawgram 2.22.0 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.js +124 -0
- package/dist/attachments.js +142 -0
- package/dist/channel.js +286 -1677
- package/dist/helpers.js +47 -0
- package/dist/inbound-pipeline.js +872 -0
- package/dist/outbound.js +260 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/outbound.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Исходящий контур канала: куда уходит текст и что при этом проверяется.
|
|
3
|
+
//
|
|
4
|
+
// Вынесено из channel.ts — файла на 3009 строк при следующем по величине
|
|
5
|
+
// 1165 (находка A6-11). Замыкание использовало отсюда ровно две вещи:
|
|
6
|
+
// карту рантаймов и журнал, поэтому блок отделяется фабрикой, а не
|
|
7
|
+
// переписыванием.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createOutbound = createOutbound;
|
|
10
|
+
const core_1 = require("openclaw/plugin-sdk/core");
|
|
11
|
+
const media_1 = require("./media");
|
|
12
|
+
const send_scope_1 = require("./send-scope");
|
|
13
|
+
const system_notice_1 = require("./system-notice");
|
|
14
|
+
const group_reply_address_1 = require("./group-reply-address");
|
|
15
|
+
const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
|
|
16
|
+
const helpers_1 = require("./helpers");
|
|
17
|
+
const helpers_2 = require("./helpers");
|
|
18
|
+
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
19
|
+
/** Исходящий контур для карты рантаймов канала. */
|
|
20
|
+
function createOutbound(runtimes) {
|
|
21
|
+
return {
|
|
22
|
+
// Core's agent-delivery path (`--deliver`, subagent announces) calls this
|
|
23
|
+
// hook under three constraints, all learned live on 2026-08-06:
|
|
24
|
+
//
|
|
25
|
+
// - `to` may be undefined (no explicit target, session route yielded
|
|
26
|
+
// none), and a rejection is NOT caught: a throw here is an unhandled
|
|
27
|
+
// rejection that takes down the entire gateway process.
|
|
28
|
+
// - `resolveAgentDeliveryPlanWithSessionRoute` calls it WITHOUT await.
|
|
29
|
+
// An async hook hands core a Promise, `promise.ok` reads undefined and
|
|
30
|
+
// the error branch dereferences `promise.error.message` — the crash
|
|
31
|
+
// every subagent announce died on. The hook must return a plain value;
|
|
32
|
+
// the call sites that do await are unaffected, await of a value works.
|
|
33
|
+
// - In a not-ok result core reads `error.message`, so the error must be
|
|
34
|
+
// Error-like, not a bare string.
|
|
35
|
+
//
|
|
36
|
+
// Peer resolution deliberately does not happen here: `sendText` resolves
|
|
37
|
+
// the peer itself, and doing it here would force the hook async again.
|
|
38
|
+
resolveTarget(ctx) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = typeof ctx.to === "string" ? ctx.to.trim() : "";
|
|
41
|
+
actionLog.info("clawgram outbound resolveTarget", {
|
|
42
|
+
accountId: ctx.accountId,
|
|
43
|
+
rawTo: raw || null,
|
|
44
|
+
});
|
|
45
|
+
if (!raw) {
|
|
46
|
+
return { ok: false, error: new Error("clawgram: no delivery target — pass `to` or use a session with a bound chat") };
|
|
47
|
+
}
|
|
48
|
+
const target = (0, helpers_1.normalizeOutboundTarget)(raw);
|
|
49
|
+
// Тот же барьер, что у `handleAction`: доставка ядра (`--deliver`,
|
|
50
|
+
// анонсы субагентов) идёт этим путём и мимо той проверки. Отказ
|
|
51
|
+
// здесь возвращается результатом, а не броском: бросок в этом хуке
|
|
52
|
+
// роняет весь gateway (грабли 06.08.2026, выше).
|
|
53
|
+
if (!(0, send_scope_1.isChatSendable)(target, (0, send_scope_1.sendScopeFor)(ctx.accountId))) {
|
|
54
|
+
const reason = (0, send_scope_1.isPhoneNumberTarget)(target)
|
|
55
|
+
? "phone-number target"
|
|
56
|
+
: "chat outside send scope";
|
|
57
|
+
actionLog.warn("clawgram outbound resolveTarget refused", {
|
|
58
|
+
accountId: ctx.accountId,
|
|
59
|
+
target,
|
|
60
|
+
reason,
|
|
61
|
+
});
|
|
62
|
+
return { ok: false, error: new Error(`clawgram: not-allowed-chat ${target}`) };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, to: target };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async sendText(ctx) {
|
|
71
|
+
// Never log `text`: outbound bodies are private correspondence and the
|
|
72
|
+
// channel log is a plain journald sink. Length is enough to tell an
|
|
73
|
+
// empty or truncated send apart from a real one.
|
|
74
|
+
actionLog.info("clawgram outbound sendText", {
|
|
75
|
+
accountId: ctx.accountId,
|
|
76
|
+
rawTo: ctx.to,
|
|
77
|
+
replyToId: ctx.replyToId ?? null,
|
|
78
|
+
threadId: ctx.threadId ?? null,
|
|
79
|
+
textLength: ctx.text.length,
|
|
80
|
+
});
|
|
81
|
+
// Core normalizes reply payloads and drops the silent token before a
|
|
82
|
+
// channel is called, so this should never see one. "Should never" is
|
|
83
|
+
// what the inbound path was assumed to be too, right until it posted a
|
|
84
|
+
// token — and the check costs a string comparison.
|
|
85
|
+
if (ctx.text.trim() && (0, helpers_1.isSilentReplyText)(ctx.text)) {
|
|
86
|
+
actionLog.info("clawgram suppressing silent outbound send", {
|
|
87
|
+
accountId: ctx.accountId,
|
|
88
|
+
rawTo: ctx.to,
|
|
89
|
+
});
|
|
90
|
+
return { skipped: "silent" };
|
|
91
|
+
}
|
|
92
|
+
// Core's operational chatter (tool-error warnings, fallback notices)
|
|
93
|
+
// stays out of group chats: it is telemetry for the operator, not a
|
|
94
|
+
// reply to the room, and it has already been seen carrying shell
|
|
95
|
+
// commands with secret-store paths. DMs keep it. The text itself is
|
|
96
|
+
// never logged — see system-notice.ts for why.
|
|
97
|
+
const suppressedNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
|
|
98
|
+
targetKind: (0, helpers_1.inferOutboundTargetKind)(ctx.to),
|
|
99
|
+
text: ctx.text,
|
|
100
|
+
to: ctx.to,
|
|
101
|
+
operatorIds: (0, system_notice_1.operatorIdsFor)(ctx.accountId),
|
|
102
|
+
});
|
|
103
|
+
if (suppressedNotice) {
|
|
104
|
+
actionLog.warn("clawgram suppressing system notice in group", {
|
|
105
|
+
accountId: ctx.accountId,
|
|
106
|
+
rawTo: ctx.to,
|
|
107
|
+
noticeKind: suppressedNotice,
|
|
108
|
+
textLength: ctx.text.length,
|
|
109
|
+
});
|
|
110
|
+
return { skipped: "system-notice" };
|
|
111
|
+
}
|
|
112
|
+
const gram = runtimes.get(ctx.accountId);
|
|
113
|
+
if (!gram) {
|
|
114
|
+
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
115
|
+
}
|
|
116
|
+
// The agent already answered this message with its own `send`, and this
|
|
117
|
+
// is core delivering the same turn's final text. Two messages for one
|
|
118
|
+
// answer is how 2026-08-10 read in a work chat: every request reported
|
|
119
|
+
// twice, in slightly different words, seconds apart.
|
|
120
|
+
//
|
|
121
|
+
// Core's own convention is that an agent which has sent a message
|
|
122
|
+
// returns NO_REPLY; this catches the turns that forget. The window is
|
|
123
|
+
// seconds wide, so a result the assistant comes back with later is
|
|
124
|
+
// still delivered.
|
|
125
|
+
if (ctx.replyToId !== null && ctx.replyToId !== undefined && (0, group_visible_reply_guard_1.hadTurnSendJustNow)({
|
|
126
|
+
accountId: ctx.accountId,
|
|
127
|
+
chatId: (0, helpers_1.normalizeOutboundTarget)(ctx.to),
|
|
128
|
+
currentMessageId: ctx.replyToId,
|
|
129
|
+
})) {
|
|
130
|
+
actionLog.warn("clawgram suppressing echo of a turn that already sent", {
|
|
131
|
+
accountId: ctx.accountId,
|
|
132
|
+
rawTo: ctx.to,
|
|
133
|
+
replyToId: ctx.replyToId,
|
|
134
|
+
textLength: ctx.text.length,
|
|
135
|
+
});
|
|
136
|
+
return { skipped: "duplicate" };
|
|
137
|
+
}
|
|
138
|
+
const groupReplyAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
|
|
139
|
+
accountId: ctx.accountId,
|
|
140
|
+
chatId: ctx.to,
|
|
141
|
+
replyToId: ctx.replyToId,
|
|
142
|
+
});
|
|
143
|
+
const targetKind = (0, helpers_1.inferOutboundTargetKind)(ctx.to);
|
|
144
|
+
const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
145
|
+
const messageThreadId = (0, helpers_2.parseOptionalThreadId)(ctx.threadId);
|
|
146
|
+
const sent = await gram.sendText({
|
|
147
|
+
target,
|
|
148
|
+
text: (0, helpers_1.prefixReplyTextToAddress)(ctx.text, groupReplyAddress),
|
|
149
|
+
targetKind,
|
|
150
|
+
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
|
|
151
|
+
messageThreadId,
|
|
152
|
+
parseMode: gram.replyParseMode,
|
|
153
|
+
});
|
|
154
|
+
actionLog.info("clawgram outbound sendText completed", {
|
|
155
|
+
accountId: ctx.accountId,
|
|
156
|
+
to: target,
|
|
157
|
+
targetKind,
|
|
158
|
+
replyToId: ctx.replyToId ?? null,
|
|
159
|
+
sentMessageId: String(sent?.id ?? ""),
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
messageId: String(sent?.id ?? ""),
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
async sendMedia(ctx) {
|
|
167
|
+
const gram = runtimes.get(ctx.accountId);
|
|
168
|
+
if (!gram) {
|
|
169
|
+
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
170
|
+
}
|
|
171
|
+
// Same rule as the action path: a local file outside the declared
|
|
172
|
+
// roots is refused before anything is uploaded.
|
|
173
|
+
const outboundRoots = ctx.mediaLocalRoots ?? ctx.mediaAccess?.localRoots;
|
|
174
|
+
(0, media_1.assertLocalMediaWithinRoots)(ctx.filePath, outboundRoots);
|
|
175
|
+
(0, media_1.assertLocalMediaWithinRoots)(ctx.mediaUrl, outboundRoots);
|
|
176
|
+
actionLog.info("clawgram outbound sendMedia", {
|
|
177
|
+
accountId: ctx.accountId,
|
|
178
|
+
rawTo: ctx.to,
|
|
179
|
+
replyToId: ctx.replyToId ?? null,
|
|
180
|
+
threadId: ctx.threadId ?? null,
|
|
181
|
+
filePath: ctx.filePath ?? null,
|
|
182
|
+
mediaUrl: ctx.mediaUrl ?? null,
|
|
183
|
+
hasText: Boolean(ctx.text),
|
|
184
|
+
hasCaption: Boolean(ctx.caption),
|
|
185
|
+
asVoice: ctx.audioAsVoice === true,
|
|
186
|
+
});
|
|
187
|
+
const file = ctx.filePath ?? ctx.mediaUrl;
|
|
188
|
+
if (!file) {
|
|
189
|
+
throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
|
|
190
|
+
}
|
|
191
|
+
// Ниже — проверки, которые у `sendText` были, а здесь не было ни
|
|
192
|
+
// одной: путь доставки медиа писался отдельно и обзавёлся только
|
|
193
|
+
// своими границами (находка A6-18).
|
|
194
|
+
const mediaTarget = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
195
|
+
// Область отправки: файл наружу — такое же исходящее, как текст.
|
|
196
|
+
// `resolveTarget` ядро зовёт не на каждом пути, поэтому проверяем и тут.
|
|
197
|
+
if (!(0, send_scope_1.isChatSendable)(mediaTarget, (0, send_scope_1.sendScopeFor)(ctx.accountId))) {
|
|
198
|
+
actionLog.warn("clawgram outbound sendMedia refused", {
|
|
199
|
+
accountId: ctx.accountId,
|
|
200
|
+
target: mediaTarget,
|
|
201
|
+
reason: (0, send_scope_1.isPhoneNumberTarget)(mediaTarget) ? "phone-number target" : "chat outside send scope",
|
|
202
|
+
});
|
|
203
|
+
return { skipped: "not-allowed" };
|
|
204
|
+
}
|
|
205
|
+
// Молчаливый ответ: подпись с токеном молчания означает «ничего не
|
|
206
|
+
// говорить», и отправлять файл с ним в подписи — тем более.
|
|
207
|
+
const mediaCaption = ctx.caption ?? ctx.text;
|
|
208
|
+
if (mediaCaption?.trim() && (0, helpers_1.isSilentReplyText)(mediaCaption)) {
|
|
209
|
+
actionLog.info("clawgram suppressing silent outbound media", {
|
|
210
|
+
accountId: ctx.accountId,
|
|
211
|
+
rawTo: ctx.to,
|
|
212
|
+
});
|
|
213
|
+
return { skipped: "silent" };
|
|
214
|
+
}
|
|
215
|
+
// Обращение в группе — то же, что у текста: адрес принадлежит
|
|
216
|
+
// конкретному входящему сообщению, а не последнему говорившему.
|
|
217
|
+
const mediaReplyAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
|
|
218
|
+
accountId: ctx.accountId,
|
|
219
|
+
chatId: ctx.to,
|
|
220
|
+
replyToId: ctx.replyToId,
|
|
221
|
+
});
|
|
222
|
+
// Чего здесь НЕТ намеренно:
|
|
223
|
+
// — подавление эха хода (`hadTurnSendJustNow`): у текста дубль стоит
|
|
224
|
+
// лишнего сообщения, а у медиа отказ стоит потерянного файла —
|
|
225
|
+
// картинку агент готовил, и второй раз она не появится;
|
|
226
|
+
// — подавление служебных сообщений ядра в группах: они текстовые,
|
|
227
|
+
// медиа-доставка ими не бывает.
|
|
228
|
+
const messageThreadId = (0, helpers_2.parseOptionalThreadId)(ctx.threadId);
|
|
229
|
+
// Same normalization `sendText` does two functions up. Without it the
|
|
230
|
+
// channel prefix reaches peer resolution and the send throws — which is
|
|
231
|
+
// exactly how a synthesized group reply died on 2026-08-08, silently
|
|
232
|
+
// enough that the transcript fallback posted it as raw text instead.
|
|
233
|
+
const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
234
|
+
const sent = await gram.sendMedia({
|
|
235
|
+
target,
|
|
236
|
+
file,
|
|
237
|
+
// Подпись получает то же обращение, что и текстовый ответ.
|
|
238
|
+
caption: mediaCaption
|
|
239
|
+
? (0, helpers_1.prefixReplyTextToAddress)(mediaCaption, mediaReplyAddress)
|
|
240
|
+
: mediaCaption,
|
|
241
|
+
// Captions follow the account reply format like every other reply:
|
|
242
|
+
// they are the same agent prose, just attached to a file (2.15.0).
|
|
243
|
+
parseMode: gram.replyParseMode,
|
|
244
|
+
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
|
|
245
|
+
messageThreadId,
|
|
246
|
+
asVoice: ctx.audioAsVoice === true,
|
|
247
|
+
});
|
|
248
|
+
actionLog.info("clawgram outbound sendMedia completed", {
|
|
249
|
+
accountId: ctx.accountId,
|
|
250
|
+
to: ctx.to,
|
|
251
|
+
replyToId: ctx.replyToId ?? null,
|
|
252
|
+
sentMessageId: String(sent?.id ?? ""),
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
messageId: String(sent?.id ?? ""),
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
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.24.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED