clawgram 2.25.0 → 2.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -9
- package/dist/account-registry.js +33 -0
- package/dist/account-scopes.js +76 -0
- package/dist/action-context.js +2 -0
- package/dist/actions-manage.js +198 -0
- package/dist/actions-read.js +394 -0
- package/dist/actions-send.js +302 -0
- package/dist/channel.js +37 -975
- package/dist/gramjs-client.js +7 -25
- package/dist/helpers.js +5 -2
- package/dist/history.js +52 -5
- package/dist/inbound-pipeline.js +68 -106
- package/dist/manage.js +1 -6
- package/dist/media.js +22 -0
- package/dist/outbound.js +22 -24
- package/dist/send-scope.js +13 -26
- package/dist/system-notice.js +0 -20
- package/npm-shrinkwrap.json +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
package/dist/channel.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createChannelPlugin = exports.canonicalAction = exports.CORE_ACTION_SYNONYMS = void 0;
|
|
7
|
-
exports.resolveAccountOperatorIds = resolveAccountOperatorIds;
|
|
3
|
+
exports.createChannelPlugin = exports.canonicalAction = exports.CORE_ACTION_SYNONYMS = exports.resolveAccountOperatorIds = void 0;
|
|
8
4
|
const core_1 = require("openclaw/plugin-sdk/core");
|
|
9
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
5
|
/**
|
|
12
6
|
* How long a file fetched by `fetch-media` stays on disk.
|
|
13
7
|
*
|
|
@@ -16,10 +10,6 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
16
10
|
* that a chat full of images does not silently become a copy of itself in the
|
|
17
11
|
* temp directory.
|
|
18
12
|
*/
|
|
19
|
-
// Час, а не сутки: `fetch-media` существует ради «прочитать и переслать», и
|
|
20
|
-
// файл нужен ровно на время хода. Сутки означали сутки чужой личной переписки
|
|
21
|
-
// на диске (A5-13).
|
|
22
|
-
const FETCHED_MEDIA_TTL_MS = 60 * 60 * 1000;
|
|
23
13
|
/**
|
|
24
14
|
* What this channel promises the Gateway.
|
|
25
15
|
*
|
|
@@ -50,53 +40,29 @@ const CHANNEL_CAPABILITIES = {
|
|
|
50
40
|
},
|
|
51
41
|
},
|
|
52
42
|
};
|
|
53
|
-
const media_1 = require("./media");
|
|
54
|
-
const fetch_media_1 = require("./fetch-media");
|
|
55
43
|
const channel_runtime_1 = require("openclaw/plugin-sdk/channel-runtime");
|
|
56
|
-
const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
|
|
57
44
|
const tool_send_1 = require("openclaw/plugin-sdk/tool-send");
|
|
58
|
-
const channel_pairing_1 = require("openclaw/plugin-sdk/channel-pairing");
|
|
59
45
|
const events_1 = require("telegram/events");
|
|
60
46
|
const gramjs_client_1 = require("./gramjs-client");
|
|
61
|
-
const
|
|
62
|
-
const send_scope_1 = require("./send-scope");
|
|
47
|
+
const account_registry_1 = require("./account-registry");
|
|
63
48
|
const joins_1 = require("./joins");
|
|
64
|
-
const reactions_1 = require("./reactions");
|
|
65
|
-
const manage_1 = require("./manage");
|
|
66
|
-
const system_notice_1 = require("./system-notice");
|
|
67
|
-
const state_dir_1 = require("./state-dir");
|
|
68
|
-
const chat_info_1 = require("./chat-info");
|
|
69
|
-
const topics_1 = require("./topics");
|
|
70
|
-
const dialogs_1 = require("./dialogs");
|
|
71
49
|
const group_tool_policy_1 = require("./group-tool-policy");
|
|
72
50
|
const secret_refs_1 = require("./secret-refs");
|
|
73
51
|
const secret_ref_runtime_1 = require("openclaw/plugin-sdk/secret-ref-runtime");
|
|
74
|
-
const group_reply_address_1 = require("./group-reply-address");
|
|
75
|
-
const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
|
|
76
52
|
const helpers_1 = require("./helpers");
|
|
77
53
|
const proxy_config_1 = require("./proxy-config");
|
|
78
54
|
const constants_1 = require("./constants");
|
|
55
|
+
const actions_1 = require("./actions");
|
|
56
|
+
Object.defineProperty(exports, "CORE_ACTION_SYNONYMS", { enumerable: true, get: function () { return actions_1.CORE_ACTION_SYNONYMS; } });
|
|
57
|
+
Object.defineProperty(exports, "canonicalAction", { enumerable: true, get: function () { return actions_1.canonicalAction; } });
|
|
58
|
+
const actions_read_1 = require("./actions-read");
|
|
59
|
+
const actions_manage_1 = require("./actions-manage");
|
|
60
|
+
const actions_send_1 = require("./actions-send");
|
|
61
|
+
const account_scopes_1 = require("./account-scopes");
|
|
62
|
+
Object.defineProperty(exports, "resolveAccountOperatorIds", { enumerable: true, get: function () { return account_scopes_1.resolveAccountOperatorIds; } });
|
|
63
|
+
const outbound_1 = require("./outbound");
|
|
64
|
+
const inbound_pipeline_1 = require("./inbound-pipeline");
|
|
79
65
|
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
80
|
-
/**
|
|
81
|
-
* Read scope as configured for the account. Left `undefined` when the key is
|
|
82
|
-
* absent so `isChatReadable` can tell "not configured" from "configured empty" —
|
|
83
|
-
* the first means no restriction, the second denies everything.
|
|
84
|
-
*/
|
|
85
|
-
function readAccountReadChats(account) {
|
|
86
|
-
const raw = account?.readChats;
|
|
87
|
-
if (raw === undefined || raw === null)
|
|
88
|
-
return undefined;
|
|
89
|
-
const entries = Array.isArray(raw) ? raw : [raw];
|
|
90
|
-
return entries.map((entry) => String(entry).trim()).filter(Boolean);
|
|
91
|
-
}
|
|
92
|
-
function resolveAccountReadChats(cfg, accountId) {
|
|
93
|
-
return readAccountReadChats(cfg?.channels?.["clawgram"]?.accounts?.[accountId]);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Outbound scope as configured. Handed to `isChatSendable` raw: an absent
|
|
97
|
-
* value means "unrestricted" and an empty list means "deny", and only the
|
|
98
|
-
* raw value tells those apart — same shape as `readChats`.
|
|
99
|
-
*/
|
|
100
66
|
/**
|
|
101
67
|
* Хэндл в `allowFrom` — обещание, которое Telegram не держит.
|
|
102
68
|
*
|
|
@@ -123,78 +89,6 @@ function warnAboutHandleAllowlistEntries(cfg, accountId) {
|
|
|
123
89
|
why: "a released handle can be taken by someone else; numeric ids do not change hands",
|
|
124
90
|
});
|
|
125
91
|
}
|
|
126
|
-
function resolveAccountSendChats(cfg, accountId) {
|
|
127
|
-
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.sendChats;
|
|
128
|
-
}
|
|
129
|
-
/** One refusal for every outbound action, so the three read the same. */
|
|
130
|
-
function refuseOutboundOutsideScope(action, accountId, target) {
|
|
131
|
-
const phone = (0, send_scope_1.isPhoneNumberTarget)(target);
|
|
132
|
-
const reason = phone ? "phone-number target" : "chat outside send scope";
|
|
133
|
-
// Телефонный номер — персональные данные: в журнал идёт вид цели, не значение (B5-09).
|
|
134
|
-
actionLog.warn(`clawgram ${action} refused: ${reason}`, { accountId, ...(phone ? { targetKind: "phone" } : { target }) });
|
|
135
|
-
throw new Error(`clawgram: not-allowed-chat ${target}`);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Management scope as configured. Handed to `isChatManageable` raw: unlike
|
|
139
|
-
* `readChats`, an absent value already means "deny", so there is nothing to
|
|
140
|
-
* tell apart here.
|
|
141
|
-
*/
|
|
142
|
-
/** Chat discovery as configured; absent means "deny", like management scope. */
|
|
143
|
-
/**
|
|
144
|
-
* Кому уходит операционная телеметрия ядра в личке.
|
|
145
|
-
*
|
|
146
|
-
* `operatorIds` — если задан. Иначе `allowFrom`, но только когда это
|
|
147
|
-
* конкретный список: со звёздочкой он означает «пишет кто угодно», и слать
|
|
148
|
-
* туда пути secret-store нельзя (A5-11). Пустой результат означает «оператор
|
|
149
|
-
* не назван», и уведомление подавляется везде.
|
|
150
|
-
*
|
|
151
|
-
* Запоминается при старте аккаунта — см. реестр в system-notice.ts.
|
|
152
|
-
*/
|
|
153
|
-
function resolveAccountOperatorIds(cfg, accountId) {
|
|
154
|
-
const account = cfg?.channels?.["clawgram"]?.accounts?.[accountId];
|
|
155
|
-
// Только явный список. Умолчание «operatorIds = allowFrom» делало
|
|
156
|
-
// оператором каждого допущенного собеседника — и телеметрию с путями
|
|
157
|
-
// secret-store получал любой из них (D2-03, A5-11). Не назван — не
|
|
158
|
-
// назван: уведомления подавляются везде.
|
|
159
|
-
const raw = account?.operatorIds;
|
|
160
|
-
if (raw === undefined || raw === null)
|
|
161
|
-
return [];
|
|
162
|
-
const entries = Array.isArray(raw) ? raw : [raw];
|
|
163
|
-
return entries.map((entry) => String(entry).trim()).filter(Boolean);
|
|
164
|
-
}
|
|
165
|
-
function resolveAccountDiscoverChats(cfg, accountId) {
|
|
166
|
-
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.discoverChats;
|
|
167
|
-
}
|
|
168
|
-
function resolveAccountManageChats(cfg, accountId) {
|
|
169
|
-
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.manageChats;
|
|
170
|
-
}
|
|
171
|
-
/** Same normalization `readChats` gets, for the resolved-account copy. */
|
|
172
|
-
function readAccountManageChats(account) {
|
|
173
|
-
const raw = account?.manageChats;
|
|
174
|
-
if (raw === undefined || raw === null)
|
|
175
|
-
return undefined;
|
|
176
|
-
const entries = Array.isArray(raw) ? raw : [raw];
|
|
177
|
-
return entries.map((entry) => String(entry).trim()).filter(Boolean);
|
|
178
|
-
}
|
|
179
|
-
const actions_1 = require("./actions");
|
|
180
|
-
Object.defineProperty(exports, "CORE_ACTION_SYNONYMS", { enumerable: true, get: function () { return actions_1.CORE_ACTION_SYNONYMS; } });
|
|
181
|
-
Object.defineProperty(exports, "canonicalAction", { enumerable: true, get: function () { return actions_1.canonicalAction; } });
|
|
182
|
-
const outbound_1 = require("./outbound");
|
|
183
|
-
const inbound_pipeline_1 = require("./inbound-pipeline");
|
|
184
|
-
/**
|
|
185
|
-
* Turns an inbound attachment into text the agent can read.
|
|
186
|
-
*
|
|
187
|
-
* The work is deliberately delegated: `runtime.mediaUnderstanding` already
|
|
188
|
-
* knows which backend this installation uses for speech and for images, so
|
|
189
|
-
* the channel stays out of that choice — a local model today, something else
|
|
190
|
-
* tomorrow, without touching this file.
|
|
191
|
-
*
|
|
192
|
-
* Failure is not an error worth dropping the message over. An attachment that
|
|
193
|
-
* could not be read still happened, and the assistant is better off saying
|
|
194
|
-
* "you sent something I could not read" than staying silent, which is
|
|
195
|
-
* indistinguishable from being offline.
|
|
196
|
-
*/
|
|
197
|
-
const attachments_1 = require("./attachments");
|
|
198
92
|
const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
199
93
|
const resolveRuntimeAccountId = (cfg, preferred) => {
|
|
200
94
|
const configured = (0, helpers_1.resolveConfiguredAccountId)(cfg, preferred);
|
|
@@ -206,20 +100,8 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
206
100
|
}
|
|
207
101
|
return configured ?? runtimes.keys().next().value;
|
|
208
102
|
};
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
*
|
|
212
|
-
* One helper instead of the eleven copies of this three-liner that used to
|
|
213
|
-
* sit inside each dispatch branch — the same repetition that made every new
|
|
214
|
-
* action cost a scaffold (finding A6-11).
|
|
215
|
-
*/
|
|
216
|
-
const requireRuntimeFor = (id) => {
|
|
217
|
-
const gram = runtimes.get(id);
|
|
218
|
-
if (!gram) {
|
|
219
|
-
throw new Error(`clawgram: runtime not found for account ${id}`);
|
|
220
|
-
}
|
|
221
|
-
return gram;
|
|
222
|
-
};
|
|
103
|
+
/** The connected runtime for an account, or a refusal naming it (A6-11, D2-11). */
|
|
104
|
+
const requireRuntimeFor = (id) => (0, account_registry_1.requireRuntime)(runtimes, id);
|
|
223
105
|
return {
|
|
224
106
|
id: "clawgram",
|
|
225
107
|
meta: {
|
|
@@ -298,7 +180,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
298
180
|
apiHash: (0, secret_refs_1.readSecretInput)(account?.apiHash),
|
|
299
181
|
sessionString: (0, secret_refs_1.readSecretInput)(account?.sessionString),
|
|
300
182
|
...(0, helpers_1.resolveAccountScopes)(cfg, accountId),
|
|
301
|
-
readChats: readAccountReadChats(account),
|
|
183
|
+
readChats: (0, account_scopes_1.readAccountReadChats)(account),
|
|
302
184
|
enabled: account?.enabled,
|
|
303
185
|
accountId,
|
|
304
186
|
proxy: (0, proxy_config_1.resolveProxyConfig)(account?.proxy),
|
|
@@ -307,7 +189,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
307
189
|
// client from a config object this function had already stripped it
|
|
308
190
|
// from, so the setting validated, deployed and did nothing.
|
|
309
191
|
replyParseMode: account?.replyParseMode,
|
|
310
|
-
manageChats: readAccountManageChats(account),
|
|
192
|
+
manageChats: (0, account_scopes_1.readAccountManageChats)(account),
|
|
311
193
|
// Optional secret: absent must stay absent, not become "".
|
|
312
194
|
twoFaPassword: account?.twoFaPassword === undefined || account?.twoFaPassword === null
|
|
313
195
|
? undefined
|
|
@@ -364,18 +246,14 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
364
246
|
const gram = new gramjs_client_1.GramJsClientManager(resolvedAccount);
|
|
365
247
|
await gram.start();
|
|
366
248
|
runtimes.set(accountId, gram);
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
(0,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
// The controller only reads core.channel.pairing, but its parameter is typed
|
|
374
|
-
// as the full PluginRuntime, and ctx (hence channelRuntime) is untyped.
|
|
375
|
-
core: { channel: channelRuntime },
|
|
376
|
-
channel: "clawgram",
|
|
377
|
-
accountId,
|
|
249
|
+
// Что `outbound.*` должен знать об аккаунте без конфига: область
|
|
250
|
+
// отправки (A5-12) и операторы (A5-11). Одна запись, снимается при
|
|
251
|
+
// остановке аккаунта (D2-11).
|
|
252
|
+
(0, account_registry_1.rememberAccount)(accountId, {
|
|
253
|
+
sendChats: (0, account_scopes_1.resolveAccountSendChats)(cfg, accountId),
|
|
254
|
+
operatorIds: (0, account_scopes_1.resolveAccountOperatorIds)(cfg, accountId),
|
|
378
255
|
});
|
|
256
|
+
warnAboutHandleAllowlistEntries(cfg, accountId);
|
|
379
257
|
const me = await gram.getMe();
|
|
380
258
|
const selfId = me?.id ? String(me.id) : undefined;
|
|
381
259
|
const selfUsername = (0, helpers_1.resolveActiveUsername)(me);
|
|
@@ -394,7 +272,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
394
272
|
const client = gram.getClient();
|
|
395
273
|
const eventBuilder = new events_1.NewMessage({});
|
|
396
274
|
const eventHandler = async (event) => (0, inbound_pipeline_1.handleInboundEvent)(event, {
|
|
397
|
-
accountId, cfg, channelRuntime, client, gram, log,
|
|
275
|
+
accountId, cfg, channelRuntime, client, gram, log,
|
|
398
276
|
pluginRuntime, runtimes, selfId, selfLabel, selfUsername,
|
|
399
277
|
});
|
|
400
278
|
client.addEventHandler(eventHandler, eventBuilder);
|
|
@@ -431,6 +309,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
431
309
|
await (0, channel_runtime_1.waitUntilAbort)(ctx.abortSignal, async () => {
|
|
432
310
|
client.removeEventHandler(eventHandler, eventBuilder);
|
|
433
311
|
client.removeEventHandler(joinEventHandler, joinEventBuilder);
|
|
312
|
+
(0, account_registry_1.forgetAccount)(accountId);
|
|
434
313
|
const runtime = runtimes.get(accountId);
|
|
435
314
|
if (!runtime) {
|
|
436
315
|
return;
|
|
@@ -618,8 +497,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
618
497
|
// bundled channels enforce them. This one used to take `filePath`
|
|
619
498
|
// verbatim, so a path naming the secret store or the config holding
|
|
620
499
|
// `sessionString` was uploaded like any attachment.
|
|
621
|
-
mediaLocalRoots, mediaAccess, }) => {
|
|
500
|
+
mediaLocalRoots, mediaReadFile, mediaAccess, }) => {
|
|
622
501
|
const allowedMediaRoots = mediaLocalRoots ?? mediaAccess?.localRoots;
|
|
502
|
+
const readMedia = mediaReadFile ?? mediaAccess?.readFile;
|
|
623
503
|
// Core passes the flag beside `params`; callers write it inside.
|
|
624
504
|
// Both count, because a rehearsal flag that is silently ignored puts
|
|
625
505
|
// a real message in a real chat — twice, so far (2.13.1).
|
|
@@ -629,835 +509,17 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
629
509
|
// decides what a name means. An unknown name stays itself and falls
|
|
630
510
|
// through to the unsupported-action error, as before.
|
|
631
511
|
const canonical = (0, actions_1.canonicalAction)(action);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const listAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
637
|
-
if (!listAccountId) {
|
|
638
|
-
throw new Error("clawgram: no configured account found");
|
|
639
|
-
}
|
|
640
|
-
// Reading is not a side effect, so a dry run still answers — reporting
|
|
641
|
-
// an empty window would look like a quiet chat rather than a no-op.
|
|
642
|
-
if (!(0, history_1.isChatReadable)(listParams.target, resolveAccountReadChats(cfg, listAccountId))) {
|
|
643
|
-
actionLog.warn("clawgram list refused: chat outside read scope", {
|
|
644
|
-
accountId: listAccountId,
|
|
645
|
-
target: listParams.target,
|
|
646
|
-
});
|
|
647
|
-
throw new Error(`clawgram: not-allowed-chat ${listParams.target}`);
|
|
648
|
-
}
|
|
649
|
-
const listGram = runtimes.get(listAccountId);
|
|
650
|
-
if (!listGram) {
|
|
651
|
-
throw new Error(`clawgram: runtime not found for account ${listAccountId}`);
|
|
652
|
-
}
|
|
653
|
-
const history = await listGram.listMessages(listParams);
|
|
654
|
-
// Metadata only. Message text is the user's correspondence and has no
|
|
655
|
-
// business in a log that is read while debugging something else.
|
|
656
|
-
actionLog.info("clawgram handleAction list completed", {
|
|
657
|
-
accountId: listAccountId,
|
|
658
|
-
target: listParams.target,
|
|
659
|
-
limit: listParams.limit,
|
|
660
|
-
since: listParams.since ?? null,
|
|
661
|
-
until: listParams.until ?? null,
|
|
662
|
-
returned: history.messages.length,
|
|
663
|
-
truncated: history.truncated,
|
|
664
|
-
});
|
|
665
|
-
return (0, core_1.jsonResult)({
|
|
666
|
-
ok: true,
|
|
667
|
-
accountId: listAccountId,
|
|
668
|
-
chatId: history.chatId ?? listParams.target,
|
|
669
|
-
count: history.messages.length,
|
|
670
|
-
truncated: history.truncated,
|
|
671
|
-
messages: history.messages,
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
// The attachment on a message that is already in a chat.
|
|
675
|
-
//
|
|
676
|
-
// `read` says a photo exists; it does not fetch it, and the inbound
|
|
677
|
-
// path only ever reads what arrives while the agent is being addressed.
|
|
678
|
-
// Everything else — a screenshot posted an hour ago, a diagram in a
|
|
679
|
-
// chat the agent reads but was not tagged in — was visible to the
|
|
680
|
-
// channel and unreachable to the agent. Same `readChats` scope as
|
|
681
|
-
// history: this must not become a way to pull bytes out of a chat the
|
|
682
|
-
// account was never allowed to read.
|
|
683
|
-
if (canonical === "fetch-media") {
|
|
684
|
-
const fetchParams = (0, fetch_media_1.parseFetchMediaParams)(params);
|
|
685
|
-
const fetchAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
686
|
-
if (!fetchAccountId) {
|
|
687
|
-
throw new Error("clawgram: no configured account found");
|
|
688
|
-
}
|
|
689
|
-
if (!(0, history_1.isChatReadable)(fetchParams.target, resolveAccountReadChats(cfg, fetchAccountId))) {
|
|
690
|
-
actionLog.warn("clawgram fetch-media refused: chat outside read scope", {
|
|
691
|
-
accountId: fetchAccountId,
|
|
692
|
-
target: fetchParams.target,
|
|
693
|
-
});
|
|
694
|
-
throw new Error(`clawgram: not-allowed-chat ${fetchParams.target}`);
|
|
695
|
-
}
|
|
696
|
-
const fetchGram = runtimes.get(fetchAccountId);
|
|
697
|
-
if (!fetchGram) {
|
|
698
|
-
throw new Error(`clawgram: runtime not found for account ${fetchAccountId}`);
|
|
699
|
-
}
|
|
700
|
-
// Fetching is a read: a dry run answers for real, the same way `read`
|
|
701
|
-
// does. Nothing leaves the machine — the file lands in a temp
|
|
702
|
-
// directory this channel prunes — so a rehearsal that reported
|
|
703
|
-
// "would fetch" would only teach the agent to ask twice.
|
|
704
|
-
const found = await fetchGram.getMessageById(fetchParams.target, fetchParams.messageId);
|
|
705
|
-
const fetchChatId = found.chatId ?? fetchParams.target;
|
|
706
|
-
if (!found.message) {
|
|
707
|
-
actionLog.info("clawgram fetch-media found no message", {
|
|
708
|
-
accountId: fetchAccountId,
|
|
709
|
-
chatId: fetchChatId,
|
|
710
|
-
messageId: fetchParams.messageId,
|
|
711
|
-
});
|
|
712
|
-
return (0, core_1.jsonResult)({
|
|
713
|
-
ok: false,
|
|
714
|
-
accountId: fetchAccountId,
|
|
715
|
-
chatId: fetchChatId,
|
|
716
|
-
messageId: String(fetchParams.messageId),
|
|
717
|
-
error: "message-not-found",
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
// `read` throws the file away, so it gets a directory of its own —
|
|
721
|
-
// the shared directory is keyed by chat and message, and deleting
|
|
722
|
-
// that path would pull the file out from under an earlier `both`
|
|
723
|
-
// fetch of the same message that handed the caller a path.
|
|
724
|
-
// Не общий /tmp: там файлы видит каждый локальный пользователь, а на
|
|
725
|
-
// этом хосте живёт ещё и раннер деплоя. Каталог состояния OpenClaw
|
|
726
|
-
// принадлежит агенту; если он не задан, остаётся /tmp — но права
|
|
727
|
-
// 0700/0600 ставятся в любом случае (A5-13).
|
|
728
|
-
// Каталог состояния принадлежит агенту; при явно заданном
|
|
729
|
-
// OPENCLAW_STATE_DIR вложения не покидают его.
|
|
730
|
-
const mediaRoot = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
731
|
-
? node_path_1.default.join((0, state_dir_1.resolveStateDir)(), "tmp")
|
|
732
|
-
: node_os_1.default.tmpdir();
|
|
733
|
-
const sharedFetchDir = node_path_1.default.join(mediaRoot, "clawgram-fetched");
|
|
734
|
-
let fetchDir = sharedFetchDir;
|
|
735
|
-
if (fetchParams.mode === "read") {
|
|
736
|
-
const { mkdtemp, mkdir } = await import("node:fs/promises");
|
|
737
|
-
await mkdir(mediaRoot, { recursive: true, mode: 0o700 });
|
|
738
|
-
fetchDir = await mkdtemp(node_path_1.default.join(mediaRoot, "clawgram-media-"));
|
|
739
|
-
}
|
|
740
|
-
else {
|
|
741
|
-
await (0, media_1.pruneFetchedMedia)(sharedFetchDir, FETCHED_MEDIA_TTL_MS, Date.now());
|
|
742
|
-
}
|
|
743
|
-
const downloaded = await (0, media_1.downloadMessageMediaToFile)({
|
|
744
|
-
client: fetchGram.getClient(),
|
|
745
|
-
message: found.message,
|
|
746
|
-
maxBytes: attachments_1.INBOUND_MEDIA_MAX_BYTES,
|
|
747
|
-
dir: fetchDir,
|
|
748
|
-
fileNameFor: ({ media, extension }) => (0, fetch_media_1.fetchedMediaFileName)({
|
|
749
|
-
chatId: fetchChatId,
|
|
750
|
-
messageId: fetchParams.messageId,
|
|
751
|
-
extension,
|
|
752
|
-
fileName: media.fileName,
|
|
753
|
-
}),
|
|
754
|
-
});
|
|
755
|
-
if (!downloaded) {
|
|
756
|
-
// Three different nothings, and the agent has to be able to tell
|
|
757
|
-
// them apart: a message with no attachment, an attachment this
|
|
758
|
-
// channel does not read (a video, a spreadsheet), and one too
|
|
759
|
-
// large to be worth the transfer. Saying "could not fetch" to all
|
|
760
|
-
// three is how "she ignored the picture" starts.
|
|
761
|
-
const described = (0, media_1.describeMedia)(found.message?.media);
|
|
762
|
-
const tooLarge = typeof described?.size === "number" && described.size > attachments_1.INBOUND_MEDIA_MAX_BYTES;
|
|
763
|
-
const error = !described
|
|
764
|
-
? "no-media"
|
|
765
|
-
: tooLarge
|
|
766
|
-
? "media-too-large"
|
|
767
|
-
: "unsupported-media";
|
|
768
|
-
actionLog.info("clawgram fetch-media returned nothing", {
|
|
769
|
-
accountId: fetchAccountId,
|
|
770
|
-
chatId: fetchChatId,
|
|
771
|
-
messageId: fetchParams.messageId,
|
|
772
|
-
kind: described?.kind ?? null,
|
|
773
|
-
error,
|
|
774
|
-
});
|
|
775
|
-
return (0, core_1.jsonResult)({
|
|
776
|
-
ok: false,
|
|
777
|
-
accountId: fetchAccountId,
|
|
778
|
-
chatId: fetchChatId,
|
|
779
|
-
messageId: String(fetchParams.messageId),
|
|
780
|
-
media: described ?? null,
|
|
781
|
-
error,
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
let read;
|
|
785
|
-
let readError;
|
|
786
|
-
if (fetchParams.mode !== "file") {
|
|
787
|
-
try {
|
|
788
|
-
read = await (0, attachments_1.understandAttachmentFile)({
|
|
789
|
-
runtime: pluginRuntime,
|
|
790
|
-
cfg,
|
|
791
|
-
filePath: downloaded.path,
|
|
792
|
-
mimeType: downloaded.mimeType,
|
|
793
|
-
understanding: downloaded.understanding,
|
|
794
|
-
});
|
|
795
|
-
if (!read) {
|
|
796
|
-
readError = "read-empty";
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
catch (err) {
|
|
800
|
-
// The bytes are already here. A failed reading is worth
|
|
801
|
-
// reporting, but it does not undo a successful fetch: the file
|
|
802
|
-
// still exists and can still be forwarded.
|
|
803
|
-
readError = String(err);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
// `read` mode is the inbound contract — the words, not the file — so
|
|
807
|
-
// the bytes go away with the answer. Any other mode keeps them:
|
|
808
|
-
// that is the whole point of asking for a path.
|
|
809
|
-
if (fetchParams.mode === "read") {
|
|
810
|
-
try {
|
|
811
|
-
const { rm } = await import("node:fs/promises");
|
|
812
|
-
await rm(fetchDir, { recursive: true, force: true });
|
|
813
|
-
}
|
|
814
|
-
catch {
|
|
815
|
-
// A file left behind is pruned within a day; failing the call
|
|
816
|
-
// over it would throw away a reading that already succeeded.
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
actionLog.info("clawgram fetch-media completed", {
|
|
820
|
-
accountId: fetchAccountId,
|
|
821
|
-
chatId: fetchChatId,
|
|
822
|
-
messageId: fetchParams.messageId,
|
|
823
|
-
mode: fetchParams.mode,
|
|
824
|
-
kind: downloaded.media.kind,
|
|
825
|
-
understanding: downloaded.understanding,
|
|
826
|
-
characters: read?.length ?? 0,
|
|
827
|
-
readError: readError ?? null,
|
|
828
|
-
});
|
|
829
|
-
return (0, core_1.jsonResult)({
|
|
830
|
-
ok: true,
|
|
831
|
-
accountId: fetchAccountId,
|
|
832
|
-
chatId: fetchChatId,
|
|
833
|
-
messageId: String(fetchParams.messageId),
|
|
834
|
-
mode: fetchParams.mode,
|
|
835
|
-
media: downloaded.media,
|
|
836
|
-
understanding: downloaded.understanding,
|
|
837
|
-
filePath: fetchParams.mode === "read" ? undefined : downloaded.path,
|
|
838
|
-
text: read,
|
|
839
|
-
readError,
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* The scaffold every chat-shaped read shares.
|
|
844
|
-
*
|
|
845
|
-
* `participants`, `topics`, `dialogs`, `joins` and `chatInfo` each
|
|
846
|
-
* spelled out the same sequence: parse, resolve the account, check a
|
|
847
|
-
* scope, fetch the runtime, call it, log counts, answer. Roughly
|
|
848
|
-
* forty lines apiece, differing in four places — which is how a new
|
|
849
|
-
* action came to cost sixty lines of scaffold and how the two gates
|
|
850
|
-
* drifted apart (finding A6-11).
|
|
851
|
-
*
|
|
852
|
-
* The gate follows from the shape rather than being restated: an
|
|
853
|
-
* action that names a chat is gated by `readChats`, `dialogs` has its
|
|
854
|
-
* own discovery gate precisely because its point is to find chats
|
|
855
|
-
* that are not in scope yet, and `joins` has none — the journal only
|
|
856
|
-
* ever holds chats this account was put into.
|
|
857
|
-
*
|
|
858
|
-
* The runtime is a getter, not a value: `joins` reads a file and must
|
|
859
|
-
* not fail merely because no runtime is connected.
|
|
860
|
-
*/
|
|
861
|
-
const runRead = async (spec) => {
|
|
862
|
-
const parsed = spec.parse();
|
|
863
|
-
const readAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
864
|
-
if (!readAccountId) {
|
|
865
|
-
throw new Error("clawgram: no configured account found");
|
|
866
|
-
}
|
|
867
|
-
const target = spec.target?.(parsed);
|
|
868
|
-
if (target !== undefined) {
|
|
869
|
-
if (!(0, history_1.isChatReadable)(target, resolveAccountReadChats(cfg, readAccountId))) {
|
|
870
|
-
actionLog.warn(`clawgram ${spec.name} refused: chat outside read scope`, {
|
|
871
|
-
accountId: readAccountId,
|
|
872
|
-
target,
|
|
873
|
-
});
|
|
874
|
-
throw new Error(`clawgram: not-allowed-chat ${target}`);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
else if (spec.discovery) {
|
|
878
|
-
if (!(0, dialogs_1.isChatDiscoveryEnabled)(resolveAccountDiscoverChats(cfg, readAccountId))) {
|
|
879
|
-
actionLog.warn(`clawgram ${spec.name} refused: chat-discovery is not enabled`, {
|
|
880
|
-
accountId: readAccountId,
|
|
881
|
-
});
|
|
882
|
-
throw new Error("clawgram: chat-discovery is not enabled");
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
const gram = () => requireRuntimeFor(readAccountId);
|
|
886
|
-
const result = await spec.run({ parsed, accountId: readAccountId, gram });
|
|
887
|
-
actionLog.info(`clawgram handleAction ${spec.name} completed`, {
|
|
888
|
-
accountId: readAccountId,
|
|
889
|
-
...spec.after(parsed, result),
|
|
890
|
-
});
|
|
891
|
-
return (0, core_1.jsonResult)({ ok: true, accountId: readAccountId, ...spec.result(parsed, result) });
|
|
512
|
+
const context = {
|
|
513
|
+
action, canonical, params, cfg, accountId, dryRun, toolContext,
|
|
514
|
+
allowedMediaRoots, readMedia, pluginRuntime,
|
|
515
|
+
resolveRuntimeAccountId, requireRuntimeFor,
|
|
892
516
|
};
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
parse: () => (0, history_1.parseListParticipantsParams)(params),
|
|
900
|
-
target: (p) => p.target,
|
|
901
|
-
run: ({ parsed, gram }) => gram().listParticipants(parsed),
|
|
902
|
-
// Counts only. Member ids are personal data and have no business in
|
|
903
|
-
// a log that is read while debugging something else.
|
|
904
|
-
after: (p, m) => ({
|
|
905
|
-
target: p.target,
|
|
906
|
-
limit: p.limit,
|
|
907
|
-
returned: m.participants.length,
|
|
908
|
-
truncated: m.truncated,
|
|
909
|
-
}),
|
|
910
|
-
result: (p, m) => ({
|
|
911
|
-
chatId: m.chatId ?? p.target,
|
|
912
|
-
count: m.participants.length,
|
|
913
|
-
truncated: m.truncated,
|
|
914
|
-
participants: m.participants,
|
|
915
|
-
}),
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
// Topic names. A forum chat is addressed by topic id, and until now an
|
|
919
|
-
// id could only be lifted off an inbound message — so a topic nobody had
|
|
920
|
-
// written in yet was unreachable, and one named in words was unfindable.
|
|
921
|
-
// Titles say what a chat is working on, so the read scope gates them.
|
|
922
|
-
if (canonical === "topics") {
|
|
923
|
-
return await runRead({
|
|
924
|
-
name: "topics",
|
|
925
|
-
parse: () => (0, topics_1.parseTopicsParams)(params),
|
|
926
|
-
target: (p) => p.target,
|
|
927
|
-
run: ({ parsed, gram }) => gram().listTopics(parsed),
|
|
928
|
-
after: (p, f) => ({
|
|
929
|
-
target: p.target,
|
|
930
|
-
limit: p.limit,
|
|
931
|
-
returned: f.topics.length,
|
|
932
|
-
truncated: f.truncated,
|
|
933
|
-
}),
|
|
934
|
-
result: (p, f) => ({
|
|
935
|
-
chatId: f.chatId ?? p.target,
|
|
936
|
-
count: f.topics.length,
|
|
937
|
-
truncated: f.truncated,
|
|
938
|
-
topics: f.topics,
|
|
939
|
-
}),
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
// Which chats this account is in. Not gated by `readChats` — the whole
|
|
943
|
-
// point is to find chats that are not in it yet — so it has a gate of
|
|
944
|
-
// its own, is metadata only, and never reports direct chats.
|
|
945
|
-
if (canonical === "dialogs") {
|
|
946
|
-
return await runRead({
|
|
947
|
-
name: "dialogs",
|
|
948
|
-
parse: () => (0, dialogs_1.parseDialogsParams)(params),
|
|
949
|
-
discovery: true,
|
|
950
|
-
run: ({ parsed, gram }) => gram().listDialogs(parsed),
|
|
951
|
-
// Counts only: which chats a person's account sits in is exactly
|
|
952
|
-
// the kind of thing that should not be sitting in a log.
|
|
953
|
-
after: (p, f) => ({ limit: p.limit, returned: f.dialogs.length, truncated: f.truncated }),
|
|
954
|
-
result: (_p, f) => ({ count: f.dialogs.length, truncated: f.truncated, dialogs: f.dialogs }),
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
// Where this account was recently added, and by whom. Reading the journal
|
|
958
|
-
// has no scope check of its own: it only ever contains chats this account
|
|
959
|
-
// was put into, which is exactly what the caller is allowed to learn.
|
|
960
|
-
if (canonical === "joins") {
|
|
961
|
-
return await runRead({
|
|
962
|
-
name: "joins",
|
|
963
|
-
parse: () => (0, joins_1.parseJoinsParams)(params),
|
|
964
|
-
// No runtime: this reads a file, and must answer with none connected.
|
|
965
|
-
run: async ({ parsed, accountId: joinsAccountId }) => (0, joins_1.selectJoinRecords)((0, joins_1.readJoinRecords)((0, joins_1.resolveJoinsJournalPath)(cfg?.channels?.["clawgram"]?.accounts?.[joinsAccountId], joinsAccountId)), parsed),
|
|
966
|
-
after: (p, selected) => ({
|
|
967
|
-
since: p.since ?? null,
|
|
968
|
-
limit: p.limit,
|
|
969
|
-
returned: selected.length,
|
|
970
|
-
}),
|
|
971
|
-
result: (_p, selected) => ({ count: selected.length, joins: selected }),
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
// Describing a chat is a read, so the same `readChats` scope that gates
|
|
975
|
-
// history gates it too — this must not become a way to learn the title
|
|
976
|
-
// and size of a chat the account was never allowed to read.
|
|
977
|
-
if (canonical === "chatInfo") {
|
|
978
|
-
return await runRead({
|
|
979
|
-
name: "chatInfo",
|
|
980
|
-
parse: () => (0, chat_info_1.parseChatInfoParams)(params, toolContext),
|
|
981
|
-
target: (p) => p.target,
|
|
982
|
-
run: async ({ parsed, gram }) => {
|
|
983
|
-
const { entity, full } = await gram().getChatInfo(parsed.target);
|
|
984
|
-
return (0, chat_info_1.describeChat)(entity, full);
|
|
985
|
-
},
|
|
986
|
-
// Type and size only. The title of a private chat is as personal as
|
|
987
|
-
// its contents and has no business in a debugging log.
|
|
988
|
-
after: (_p, info) => ({
|
|
989
|
-
type: info.type,
|
|
990
|
-
memberCount: info.memberCount ?? null,
|
|
991
|
-
isForum: info.isForum ?? null,
|
|
992
|
-
}),
|
|
993
|
-
result: (p, info) => ({ chat: { ...info, chatId: info.chatId ?? p.target } }),
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
// A reaction is an outbound act on someone else's message, so it is
|
|
997
|
-
// gated like sending rather than like reading — and it respects
|
|
998
|
-
// `dryRun`, which reading does not need to.
|
|
999
|
-
if (canonical === "react") {
|
|
1000
|
-
const reactionParams = (0, reactions_1.parseReactionParams)(params, toolContext);
|
|
1001
|
-
const reactionAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1002
|
-
if (!reactionAccountId) {
|
|
1003
|
-
throw new Error("clawgram: no configured account found");
|
|
1004
|
-
}
|
|
1005
|
-
// Реакция — видимое действие от имени владельца в чужом чате, и
|
|
1006
|
-
// адресуется она так же, как сообщение: та же область (A5-12).
|
|
1007
|
-
if (!(0, send_scope_1.isChatSendable)(reactionParams.target, resolveAccountSendChats(cfg, reactionAccountId))) {
|
|
1008
|
-
refuseOutboundOutsideScope("react", reactionAccountId, String(reactionParams.target));
|
|
1009
|
-
}
|
|
1010
|
-
actionLog.info("clawgram handleAction react", {
|
|
1011
|
-
accountId: reactionAccountId,
|
|
1012
|
-
dryRun: dryRun === true,
|
|
1013
|
-
target: reactionParams.target,
|
|
1014
|
-
messageId: reactionParams.messageId,
|
|
1015
|
-
remove: reactionParams.remove,
|
|
1016
|
-
});
|
|
1017
|
-
if (dryRun === true) {
|
|
1018
|
-
return (0, core_1.jsonResult)({
|
|
1019
|
-
ok: true,
|
|
1020
|
-
dryRun: true,
|
|
1021
|
-
accountId: reactionAccountId,
|
|
1022
|
-
chatId: reactionParams.target,
|
|
1023
|
-
messageId: reactionParams.messageId,
|
|
1024
|
-
removed: reactionParams.remove,
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
const reactionGram = runtimes.get(reactionAccountId);
|
|
1028
|
-
if (!reactionGram) {
|
|
1029
|
-
throw new Error(`clawgram: runtime not found for account ${reactionAccountId}`);
|
|
1030
|
-
}
|
|
1031
|
-
await reactionGram.sendReaction(reactionParams);
|
|
1032
|
-
return (0, core_1.jsonResult)({
|
|
1033
|
-
ok: true,
|
|
1034
|
-
accountId: reactionAccountId,
|
|
1035
|
-
chatId: reactionParams.target,
|
|
1036
|
-
messageId: reactionParams.messageId,
|
|
1037
|
-
removed: reactionParams.remove,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
// ---- Chat management (2.12.0) ----
|
|
1041
|
-
//
|
|
1042
|
-
// Assembling a chat rather than speaking in it: create a supergroup,
|
|
1043
|
-
// add and remove people, appoint admins, hand the chat over, issue an
|
|
1044
|
-
// invite link. All of it is possible only because this is a personal
|
|
1045
|
-
// MTProto account — a bot could do almost none of this.
|
|
1046
|
-
//
|
|
1047
|
-
// Every branch is gated by the account's `manageChats` scope, which is
|
|
1048
|
-
// opt-in (absent = deny, see manage.ts) — these are the first actions
|
|
1049
|
-
// that change a chat rather than write into it. Parsing runs before
|
|
1050
|
-
// the gate so a malformed call fails on its own shape, and `dryRun`
|
|
1051
|
-
// returns after the gate so a dry run exercises the same refusals a
|
|
1052
|
-
// real call would hit. People's ids stay out of the logs throughout;
|
|
1053
|
-
// the JSON result carries them to the caller, the journal does not.
|
|
1054
|
-
const manageAction = actions_1.MANAGE_ACTIONS.has(canonical) ? canonical : undefined;
|
|
1055
|
-
if (manageAction) {
|
|
1056
|
-
const manageAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1057
|
-
if (!manageAccountId) {
|
|
1058
|
-
throw new Error("clawgram: no configured account found");
|
|
1059
|
-
}
|
|
1060
|
-
const manageScope = resolveAccountManageChats(cfg, manageAccountId);
|
|
1061
|
-
const requireRuntime = () => requireRuntimeFor(manageAccountId);
|
|
1062
|
-
/**
|
|
1063
|
-
* The scaffold every management action shares.
|
|
1064
|
-
*
|
|
1065
|
-
* Six actions used to spell it out one after another: resolve the
|
|
1066
|
-
* account, check the scope, log, answer a dry run, call the
|
|
1067
|
-
* runtime, log again, build the result. A change to any of those —
|
|
1068
|
-
* the dry-run contract, say — was a six-place edit in the plugin's
|
|
1069
|
-
* largest file, and the one deliberate exception (createGroup does
|
|
1070
|
-
* not check a chat scope, because the chat does not exist yet) was
|
|
1071
|
-
* invisible among the copies (finding A12-06).
|
|
1072
|
-
*
|
|
1073
|
-
* The differences stay written at each call site: what to parse,
|
|
1074
|
-
* what to log, what to run, what to answer. Only the scaffold moved.
|
|
1075
|
-
*/
|
|
1076
|
-
const runManage = async (spec) => {
|
|
1077
|
-
const parsed = spec.parse();
|
|
1078
|
-
const target = spec.target(parsed);
|
|
1079
|
-
if (target === undefined) {
|
|
1080
|
-
// Nothing to check a scope against yet, so the gate is coarser:
|
|
1081
|
-
// management must be enabled at all for this account.
|
|
1082
|
-
if (!(0, manage_1.isManagementEnabled)(manageScope)) {
|
|
1083
|
-
actionLog.warn(`clawgram ${spec.name} refused: management is not enabled`, {
|
|
1084
|
-
accountId: manageAccountId,
|
|
1085
|
-
});
|
|
1086
|
-
throw new Error("clawgram: chat management is not enabled for this account — "
|
|
1087
|
-
+ `set channels.clawgram.accounts.${manageAccountId}.manageChats`);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
else if (!(0, manage_1.isChatManageable)(target, manageScope)) {
|
|
1091
|
-
actionLog.warn("clawgram management refused: chat outside manage scope", {
|
|
1092
|
-
accountId: manageAccountId,
|
|
1093
|
-
action: manageAction,
|
|
1094
|
-
target,
|
|
1095
|
-
});
|
|
1096
|
-
throw new Error(`clawgram: not-managed-chat ${target}`);
|
|
1097
|
-
}
|
|
1098
|
-
actionLog.info(`clawgram handleAction ${spec.name}`, {
|
|
1099
|
-
accountId: manageAccountId,
|
|
1100
|
-
dryRun: dryRun === true,
|
|
1101
|
-
...spec.before(parsed),
|
|
1102
|
-
});
|
|
1103
|
-
if (dryRun === true) {
|
|
1104
|
-
return (0, core_1.jsonResult)({
|
|
1105
|
-
ok: true,
|
|
1106
|
-
dryRun: true,
|
|
1107
|
-
accountId: manageAccountId,
|
|
1108
|
-
...(target === undefined ? {} : { chatId: target }),
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
const gram = requireRuntime();
|
|
1112
|
-
spec.precondition?.(gram);
|
|
1113
|
-
const result = await spec.run(gram, parsed);
|
|
1114
|
-
actionLog.info(`clawgram handleAction ${spec.name} completed`, {
|
|
1115
|
-
accountId: manageAccountId,
|
|
1116
|
-
...spec.after(parsed, result),
|
|
1117
|
-
});
|
|
1118
|
-
return (0, core_1.jsonResult)({ ok: true, accountId: manageAccountId, ...spec.result(parsed, result) });
|
|
1119
|
-
};
|
|
1120
|
-
if (manageAction === "createGroup") {
|
|
1121
|
-
return await runManage({
|
|
1122
|
-
name: "createGroup",
|
|
1123
|
-
parse: () => (0, manage_1.parseCreateGroupParams)(params),
|
|
1124
|
-
// A group being created is not in any scope yet.
|
|
1125
|
-
target: () => undefined,
|
|
1126
|
-
before: (p) => ({ users: p.users.length, hasAbout: Boolean(p.about) }),
|
|
1127
|
-
run: (gram, p) => gram.createGroup(p),
|
|
1128
|
-
after: (_p, created) => ({ chatId: created.chatId ?? null, missing: created.missing.length }),
|
|
1129
|
-
result: (_p, created) => ({ chatId: created.chatId, missing: created.missing }),
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
if (manageAction === "addMembers") {
|
|
1133
|
-
return await runManage({
|
|
1134
|
-
name: "addMembers",
|
|
1135
|
-
parse: () => (0, manage_1.parseAddMembersParams)(params, toolContext),
|
|
1136
|
-
target: (p) => p.target,
|
|
1137
|
-
before: (p) => ({ target: p.target, users: p.users.length }),
|
|
1138
|
-
run: (gram, p) => gram.addChatMembers(p),
|
|
1139
|
-
after: (p, added) => ({
|
|
1140
|
-
target: p.target,
|
|
1141
|
-
requested: p.users.length,
|
|
1142
|
-
missing: added.missing.length,
|
|
1143
|
-
}),
|
|
1144
|
-
result: (p, added) => ({
|
|
1145
|
-
chatId: added.chatId ?? p.target,
|
|
1146
|
-
requested: p.users.length,
|
|
1147
|
-
// Telegram refuses silently-restricted invites per user; the
|
|
1148
|
-
// caller gets the ids so it can hand them an invite link.
|
|
1149
|
-
missing: added.missing,
|
|
1150
|
-
}),
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
if (manageAction === "removeMember") {
|
|
1154
|
-
return await runManage({
|
|
1155
|
-
name: "removeMember",
|
|
1156
|
-
parse: () => (0, manage_1.parseRemoveMemberParams)(params, toolContext),
|
|
1157
|
-
target: (p) => p.target,
|
|
1158
|
-
before: (p) => ({ target: p.target, ban: p.ban }),
|
|
1159
|
-
run: (gram, p) => gram.removeChatMember(p),
|
|
1160
|
-
after: (p) => ({ target: p.target, ban: p.ban }),
|
|
1161
|
-
result: (p) => ({ chatId: p.target, user: p.user, banned: p.ban }),
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
if (manageAction === "promoteAdmin" || manageAction === "demoteAdmin") {
|
|
1165
|
-
const promote = manageAction === "promoteAdmin";
|
|
1166
|
-
return await runManage({
|
|
1167
|
-
// Both spellings log as `setAdmin`, as they always have.
|
|
1168
|
-
name: "setAdmin",
|
|
1169
|
-
parse: () => (promote
|
|
1170
|
-
? (0, manage_1.parsePromoteAdminParams)(params, toolContext)
|
|
1171
|
-
: (0, manage_1.parseDemoteAdminParams)(params, toolContext)),
|
|
1172
|
-
target: (p) => p.target,
|
|
1173
|
-
before: (p) => ({ target: p.target, isAdmin: p.isAdmin, hasRank: Boolean(p.rank) }),
|
|
1174
|
-
run: (gram, p) => gram.setChatAdmin(p),
|
|
1175
|
-
after: (p) => ({ target: p.target, isAdmin: p.isAdmin }),
|
|
1176
|
-
result: (p) => ({
|
|
1177
|
-
chatId: p.target,
|
|
1178
|
-
user: p.user,
|
|
1179
|
-
isAdmin: p.isAdmin,
|
|
1180
|
-
...(p.rank ? { rank: p.rank } : {}),
|
|
1181
|
-
}),
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
if (manageAction === "transferOwnership") {
|
|
1185
|
-
return await runManage({
|
|
1186
|
-
name: "transferOwnership",
|
|
1187
|
-
parse: () => (0, manage_1.parseTransferOwnershipParams)(params, toolContext),
|
|
1188
|
-
target: (p) => p.target,
|
|
1189
|
-
before: (p) => ({ target: p.target }),
|
|
1190
|
-
// The password stays inside the runtime: it is read from the
|
|
1191
|
-
// account config at start-up and never travels through dispatch
|
|
1192
|
-
// arguments, which are one log call away from the journal.
|
|
1193
|
-
precondition: (gram) => {
|
|
1194
|
-
if (!gram.twoFaPassword) {
|
|
1195
|
-
throw new Error("clawgram: ownership transfer requires twoFaPassword in the account config "
|
|
1196
|
-
+ "(the account's Telegram 2FA password, as a literal or a SecretRef)");
|
|
1197
|
-
}
|
|
1198
|
-
},
|
|
1199
|
-
run: (gram, p) => gram.transferChatOwnership(p),
|
|
1200
|
-
after: (p) => ({ target: p.target }),
|
|
1201
|
-
result: (p) => ({ chatId: p.target, newOwner: p.user }),
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
// inviteLink — the only management action left.
|
|
1205
|
-
return await runManage({
|
|
1206
|
-
name: "inviteLink",
|
|
1207
|
-
parse: () => (0, manage_1.parseInviteLinkParams)(params, toolContext),
|
|
1208
|
-
target: (p) => p.target,
|
|
1209
|
-
before: (p) => ({
|
|
1210
|
-
target: p.target,
|
|
1211
|
-
hasExpiry: p.expireDate !== undefined,
|
|
1212
|
-
usageLimit: p.usageLimit ?? null,
|
|
1213
|
-
requestNeeded: p.requestNeeded,
|
|
1214
|
-
}),
|
|
1215
|
-
run: (gram, p) => gram.exportChatInviteLink(p),
|
|
1216
|
-
after: (p, exported) => ({ target: p.target, hasLink: Boolean(exported.link) }),
|
|
1217
|
-
result: (p, exported) => ({ chatId: p.target, link: exported.link }),
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
// Core normalizes whichever of these it filled in to a local path (see
|
|
1221
|
-
// `mediaSourceParams` above); `mediaUrl` stays a URL, which GramJS
|
|
1222
|
-
// accepts as well.
|
|
1223
|
-
const attachedFile = (0, param_readers_1.readStringParam)(params, "filePath")
|
|
1224
|
-
?? (0, param_readers_1.readStringParam)(params, "path")
|
|
1225
|
-
?? (0, param_readers_1.readStringParam)(params, "media")
|
|
1226
|
-
?? (0, param_readers_1.readStringParam)(params, "mediaUrl");
|
|
1227
|
-
// Core dispatches `upload-file`; `sendAttachment` is its legacy alias
|
|
1228
|
-
// and arrives from older callers. A plain `send` carrying a file lands
|
|
1229
|
-
// here too — `openclaw message send --media` does exactly that, and
|
|
1230
|
-
// routing it to the text path dropped the file without a word.
|
|
1231
|
-
if (canonical === "upload-file" || (canonical === "send" && attachedFile)) {
|
|
1232
|
-
const rawUploadTo = (0, helpers_1.resolveActionTarget)(params, toolContext);
|
|
1233
|
-
const uploadTo = (0, helpers_1.normalizeOutboundTarget)(rawUploadTo);
|
|
1234
|
-
const uploadAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1235
|
-
if (!uploadAccountId) {
|
|
1236
|
-
throw new Error("clawgram: no configured account found");
|
|
1237
|
-
}
|
|
1238
|
-
// Та же граница, что у `send`: файл наружу — такое же исходящее.
|
|
1239
|
-
if (!(0, send_scope_1.isChatSendable)(uploadTo, resolveAccountSendChats(cfg, uploadAccountId))) {
|
|
1240
|
-
refuseOutboundOutsideScope("upload-file", uploadAccountId, uploadTo);
|
|
1241
|
-
}
|
|
1242
|
-
const file = attachedFile;
|
|
1243
|
-
if (!file) {
|
|
1244
|
-
throw new Error("clawgram: upload-file requires filePath, path, media, or mediaUrl");
|
|
1245
|
-
}
|
|
1246
|
-
// Before anything else about the message is considered: an
|
|
1247
|
-
// out-of-scope path is refused, not sent and then regretted.
|
|
1248
|
-
(0, media_1.assertLocalMediaWithinRoots)(file, allowedMediaRoots);
|
|
1249
|
-
const captionText = (0, helpers_1.readMessageText)(params) || ((0, param_readers_1.readStringParam)(params, "caption") ?? "");
|
|
1250
|
-
// A caption is optional, but the silent-reply sentinel must never
|
|
1251
|
-
// reach Telegram as one — same reasoning as the `send` path below.
|
|
1252
|
-
const caption = captionText.trim() && (0, helpers_1.isSilentReplyText)(captionText)
|
|
1253
|
-
? ""
|
|
1254
|
-
: captionText.replaceAll("\\n", "\n");
|
|
1255
|
-
const uploadReplyToId = (0, param_readers_1.readStringOrNumberParam)(params, "replyToId") ?? (0, param_readers_1.readStringOrNumberParam)(params, "replyTo");
|
|
1256
|
-
const uploadThreadId = (0, param_readers_1.readStringOrNumberParam)(params, "threadId");
|
|
1257
|
-
const asVoice = (0, helpers_1.readVoiceNoteFlag)(params);
|
|
1258
|
-
actionLog.info("clawgram handleAction upload-file", {
|
|
1259
|
-
accountId: uploadAccountId,
|
|
1260
|
-
dryRun: dryRun === true,
|
|
1261
|
-
to: uploadTo,
|
|
1262
|
-
hasCaption: Boolean(caption),
|
|
1263
|
-
replyToId: uploadReplyToId ?? null,
|
|
1264
|
-
threadId: uploadThreadId ?? null,
|
|
1265
|
-
asVoice,
|
|
1266
|
-
});
|
|
1267
|
-
if (dryRun === true) {
|
|
1268
|
-
return (0, core_1.jsonResult)({
|
|
1269
|
-
ok: true,
|
|
1270
|
-
dryRun: true,
|
|
1271
|
-
to: uploadTo,
|
|
1272
|
-
accountId: uploadAccountId,
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
const uploadGram = runtimes.get(uploadAccountId);
|
|
1276
|
-
if (!uploadGram) {
|
|
1277
|
-
throw new Error(`clawgram: runtime not found for account ${uploadAccountId}`);
|
|
1278
|
-
}
|
|
1279
|
-
const uploaded = await uploadGram.sendMedia({
|
|
1280
|
-
target: uploadTo,
|
|
1281
|
-
file,
|
|
1282
|
-
caption: caption || undefined,
|
|
1283
|
-
// Same resolution as the text `send`: per-call value wins, an
|
|
1284
|
-
// omitted one inherits the account format (2.15.0). A caption is
|
|
1285
|
-
// the same prose as a message and renders identically.
|
|
1286
|
-
parseMode: (0, helpers_1.resolveOutboundParseMode)(params, cfg, uploadAccountId),
|
|
1287
|
-
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(rawUploadTo, uploadReplyToId),
|
|
1288
|
-
messageThreadId: (0, helpers_1.parseOptionalThreadId)(uploadThreadId),
|
|
1289
|
-
asVoice,
|
|
1290
|
-
});
|
|
1291
|
-
actionLog.info("clawgram handleAction upload-file completed", {
|
|
1292
|
-
accountId: uploadAccountId,
|
|
1293
|
-
to: uploadTo,
|
|
1294
|
-
sentMessageId: String(uploaded?.id ?? ""),
|
|
1295
|
-
});
|
|
1296
|
-
return (0, core_1.jsonResult)({
|
|
1297
|
-
ok: true,
|
|
1298
|
-
to: uploadTo,
|
|
1299
|
-
accountId: uploadAccountId,
|
|
1300
|
-
messageId: String(uploaded?.id ?? ""),
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
if (action !== "send") {
|
|
1304
|
-
throw new Error(`clawgram: unsupported message action ${action}`);
|
|
1305
|
-
}
|
|
1306
|
-
const rawTo = (0, helpers_1.resolveActionTarget)(params, toolContext);
|
|
1307
|
-
const targetKind = (0, helpers_1.inferOutboundTargetKind)(rawTo);
|
|
1308
|
-
const to = (0, helpers_1.normalizeOutboundTarget)(rawTo);
|
|
1309
|
-
const replyToId = (0, param_readers_1.readStringOrNumberParam)(params, "replyToId") ?? (0, param_readers_1.readStringOrNumberParam)(params, "replyTo");
|
|
1310
|
-
const threadId = (0, param_readers_1.readStringOrNumberParam)(params, "threadId");
|
|
1311
|
-
const messageThreadId = (0, helpers_1.parseOptionalThreadId)(threadId);
|
|
1312
|
-
// Omitting parseMode inherits the account's configured mode rather
|
|
1313
|
-
// than falling back to plain text (2.13.0): an account set to `html`
|
|
1314
|
-
// used to render replies as HTML and these sends as raw markup.
|
|
1315
|
-
const parseMode = (0, helpers_1.resolveOutboundParseMode)(params, cfg, (0, helpers_1.resolveConfiguredAccountId)(cfg, accountId) ?? accountId ?? "default");
|
|
1316
|
-
actionLog.info("clawgram handleAction send", {
|
|
1317
|
-
requestedAccountId: accountId,
|
|
1318
|
-
dryRun: dryRun === true,
|
|
1319
|
-
rawTo,
|
|
1320
|
-
to,
|
|
1321
|
-
targetKind,
|
|
1322
|
-
replyToId: replyToId ?? null,
|
|
1323
|
-
threadId: threadId ?? null,
|
|
1324
|
-
parseMode: parseMode ?? null,
|
|
1325
|
-
toolContextCurrentChannelId: toolContext?.currentChannelId ?? null,
|
|
1326
|
-
});
|
|
1327
|
-
const resolvedAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1328
|
-
if (!resolvedAccountId) {
|
|
1329
|
-
throw new Error("clawgram: no configured account found");
|
|
1330
|
-
}
|
|
1331
|
-
// Проверка ПОСЛЕ резолва аккаунта и ДО любой доставки: область задаётся
|
|
1332
|
-
// на аккаунт, а отказ должен случиться раньше, чем цель разрешена в
|
|
1333
|
-
// Telegram-сущность — resolve сам по себе виден собеседнику (A5-12).
|
|
1334
|
-
if (!(0, send_scope_1.isChatSendable)(to, resolveAccountSendChats(cfg, resolvedAccountId))) {
|
|
1335
|
-
refuseOutboundOutsideScope("send", resolvedAccountId, to);
|
|
1336
|
-
}
|
|
1337
|
-
const currentChannelId = toolContext?.currentChannelId?.trim() ?? "";
|
|
1338
|
-
const currentMessageId = toolContext?.currentMessageId;
|
|
1339
|
-
const currentChannelTarget = currentChannelId ? (0, helpers_1.normalizeOutboundTarget)(currentChannelId) : "";
|
|
1340
|
-
const sendingToCurrentGroup = Boolean(currentChannelTarget &&
|
|
1341
|
-
currentChannelTarget === to &&
|
|
1342
|
-
targetKind === "group");
|
|
1343
|
-
if (sendingToCurrentGroup &&
|
|
1344
|
-
!replyToId &&
|
|
1345
|
-
currentMessageId !== null &&
|
|
1346
|
-
currentMessageId !== undefined &&
|
|
1347
|
-
(0, group_visible_reply_guard_1.hasRecentVisibleGroupReply)({
|
|
1348
|
-
accountId: resolvedAccountId,
|
|
1349
|
-
chatId: to,
|
|
1350
|
-
currentMessageId,
|
|
1351
|
-
})) {
|
|
1352
|
-
actionLog.warn("clawgram suppressing duplicate visible group reply", {
|
|
1353
|
-
accountId: resolvedAccountId,
|
|
1354
|
-
to,
|
|
1355
|
-
currentMessageId: String(currentMessageId),
|
|
1356
|
-
toolContextCurrentChannelId: currentChannelId || null,
|
|
1357
|
-
});
|
|
1358
|
-
// A dry run reports the suppression instead of impersonating it: the
|
|
1359
|
-
// caller asked what would happen, and what would happen is nothing.
|
|
1360
|
-
return (0, core_1.jsonResult)({
|
|
1361
|
-
ok: true,
|
|
1362
|
-
...(dryRun ? { dryRun: true } : {}),
|
|
1363
|
-
suppressedDuplicate: true,
|
|
1364
|
-
to,
|
|
1365
|
-
accountId: resolvedAccountId,
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
// Whom to greet is decided by the message this turn is answering, not
|
|
1369
|
-
// by whoever spoke last. An agent replying to a request rarely passes
|
|
1370
|
-
// `replyToId`, and until 2026-08-10 that fell through to the most
|
|
1371
|
-
// recent sender: in an interleaved chat the owner's report went out
|
|
1372
|
-
// addressed to a colleague who had asked something else entirely.
|
|
1373
|
-
// A dry run peeks: consuming the address here left the real send with
|
|
1374
|
-
// no greeting, so a rehearsal silently changed the message that went
|
|
1375
|
-
// out afterwards.
|
|
1376
|
-
const groupReplyAddress = (dryRun ? group_reply_address_1.peekGroupReplyAddress : group_reply_address_1.consumeGroupReplyAddress)({
|
|
1377
|
-
accountId: resolvedAccountId,
|
|
1378
|
-
chatId: to,
|
|
1379
|
-
replyToId: replyToId ?? currentMessageId,
|
|
1380
|
-
});
|
|
1381
|
-
const requestedText = (0, helpers_1.readMessageText)(params).replaceAll("\\n", "\n");
|
|
1382
|
-
// `NO_REPLY` is OpenClaw's "say nothing" sentinel. The inbound pipeline
|
|
1383
|
-
// and core both strip it, but an explicit `message.action` call is
|
|
1384
|
-
// neither path — and the SDK itself prompts agents to send a message
|
|
1385
|
-
// and *then* answer NO_REPLY, so the two are one slip apart. Posting
|
|
1386
|
-
// the token into a work chat looks like the assistant malfunctioning.
|
|
1387
|
-
//
|
|
1388
|
-
// Checked before the reply-address prefix on purpose: prefixing first
|
|
1389
|
-
// leaves "Name: " behind, which is not empty, and the token goes out.
|
|
1390
|
-
// That is precisely how it once reached the inbound path.
|
|
1391
|
-
if (requestedText.trim() && (0, helpers_1.isSilentReplyText)(requestedText)) {
|
|
1392
|
-
actionLog.info("clawgram suppressing silent send", {
|
|
1393
|
-
accountId: resolvedAccountId,
|
|
1394
|
-
to,
|
|
1395
|
-
});
|
|
1396
|
-
return (0, core_1.jsonResult)({
|
|
1397
|
-
ok: true,
|
|
1398
|
-
skipped: "silent",
|
|
1399
|
-
sent: false,
|
|
1400
|
-
to,
|
|
1401
|
-
accountId: resolvedAccountId,
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
const text = (0, helpers_1.prefixReplyTextToAddress)(requestedText, groupReplyAddress);
|
|
1405
|
-
if (!text) {
|
|
1406
|
-
throw new Error("clawgram: message text is required");
|
|
1407
|
-
}
|
|
1408
|
-
if (dryRun) {
|
|
1409
|
-
return (0, core_1.jsonResult)({
|
|
1410
|
-
ok: true,
|
|
1411
|
-
dryRun: true,
|
|
1412
|
-
to,
|
|
1413
|
-
accountId: resolvedAccountId,
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
const gram = runtimes.get(resolvedAccountId);
|
|
1417
|
-
if (!gram) {
|
|
1418
|
-
throw new Error(`clawgram: runtime not found for account ${resolvedAccountId}`);
|
|
1419
|
-
}
|
|
1420
|
-
const sent = await gram.sendText({
|
|
1421
|
-
target: to,
|
|
1422
|
-
text,
|
|
1423
|
-
targetKind,
|
|
1424
|
-
replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(rawTo, replyToId),
|
|
1425
|
-
messageThreadId,
|
|
1426
|
-
parseMode,
|
|
1427
|
-
});
|
|
1428
|
-
if (sendingToCurrentGroup &&
|
|
1429
|
-
!replyToId &&
|
|
1430
|
-
currentMessageId !== null &&
|
|
1431
|
-
currentMessageId !== undefined) {
|
|
1432
|
-
(0, group_visible_reply_guard_1.rememberVisibleGroupReply)({
|
|
1433
|
-
accountId: resolvedAccountId,
|
|
1434
|
-
chatId: to,
|
|
1435
|
-
currentMessageId,
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
// The turn has now spoken for itself. Recorded for every send into the
|
|
1439
|
-
// chat this turn came from — with or without an explicit replyToId —
|
|
1440
|
-
// so that core delivering the turn's final text a few seconds later
|
|
1441
|
-
// can be recognised as an echo of this same answer.
|
|
1442
|
-
if (currentMessageId !== null && currentMessageId !== undefined) {
|
|
1443
|
-
(0, group_visible_reply_guard_1.rememberTurnSend)({
|
|
1444
|
-
accountId: resolvedAccountId,
|
|
1445
|
-
chatId: to,
|
|
1446
|
-
currentMessageId,
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
actionLog.info("clawgram handleAction send completed", {
|
|
1450
|
-
accountId: resolvedAccountId,
|
|
1451
|
-
to,
|
|
1452
|
-
replyToId: replyToId ?? null,
|
|
1453
|
-
sentMessageId: String(sent?.id ?? ""),
|
|
1454
|
-
});
|
|
1455
|
-
return (0, core_1.jsonResult)({
|
|
1456
|
-
ok: true,
|
|
1457
|
-
to,
|
|
1458
|
-
accountId: resolvedAccountId,
|
|
1459
|
-
messageId: String(sent?.id ?? ""),
|
|
1460
|
-
});
|
|
517
|
+
// Reads first, then management, then the outbound actions — which end
|
|
518
|
+
// with `send` and refuse whatever nobody claimed. Each module answers
|
|
519
|
+
// `undefined` for an action that is not its own (B5-13, part 3).
|
|
520
|
+
return (await (0, actions_read_1.handleReadAction)(context))
|
|
521
|
+
?? (await (0, actions_manage_1.handleManageAction)(context))
|
|
522
|
+
?? (await (0, actions_send_1.handleSendAction)(context));
|
|
1461
523
|
},
|
|
1462
524
|
},
|
|
1463
525
|
outbound: (0, outbound_1.createOutbound)(runtimes),
|