clawgram 2.25.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 +38 -9
- package/dist/account-registry.js +33 -0
- package/dist/channel.js +40 -91
- 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/README.md
CHANGED
|
@@ -714,6 +714,35 @@ Bindings
|
|
|
714
714
|
```
|
|
715
715
|
|
|
716
716
|
|
|
717
|
+
## Actions and the names that reach them
|
|
718
|
+
|
|
719
|
+
Two grammars name the same actions. The **native** names (`read`, `participants`, `topics`,
|
|
720
|
+
`dialogs`, `chatInfo`, `fetch-media`, `createGroup`, …) are what the gateway RPC, the tests and
|
|
721
|
+
this README use. The agent's `message` tool, however, only dispatches names from **core's own
|
|
722
|
+
vocabulary** (`CHANNEL_MESSAGE_ACTION_NAMES`), so every action worth reaching from a prompt also
|
|
723
|
+
answers to core's nearest name. `src/actions.ts` is the single source of both lists
|
|
724
|
+
(`ACTION_ALIASES`, `CORE_VOCABULARY_SPELLINGS`); that core really knows each name is asserted
|
|
725
|
+
against the installed core in `test/core-action-synonyms.test.ts`, so a core release that drops
|
|
726
|
+
one fails the suite rather than the chat.
|
|
727
|
+
|
|
728
|
+
| Does | Native name | Callable from the `message` tool as | Gate |
|
|
729
|
+
|---|---|---|---|
|
|
730
|
+
| send text | `send` | `send` | `sendChats` |
|
|
731
|
+
| send a file | `upload-file` | `upload-file`, `sendAttachment` | `sendChats`, media roots |
|
|
732
|
+
| react to a message | `react` | `react` | `sendChats` |
|
|
733
|
+
| read history | `read` (also `list`) | `read` | `readChats` |
|
|
734
|
+
| fetch an attachment | `fetch-media` | `download-file` | `readChats` |
|
|
735
|
+
| list members | `participants` | `member-info` | `readChats` |
|
|
736
|
+
| list forum topics | `topics` | `thread-list` | `readChats` |
|
|
737
|
+
| list chats | `dialogs` | `channel-list` | `discoverChats` |
|
|
738
|
+
| describe a chat | `chatInfo` | `channel-info` (the chat arrives in `channelId`) | `readChats` |
|
|
739
|
+
| where the account was added | `joins` | — (gateway RPC only) | — |
|
|
740
|
+
| chat management | see the table below | see the table below | `manageChats` |
|
|
741
|
+
|
|
742
|
+
Names outside core's vocabulary (`joins`, `transferOwnership`, `inviteLink`) are reachable through
|
|
743
|
+
the gateway RPC only. A name core does not know fails as "requires a target" and "does not accept a
|
|
744
|
+
target" at once — there is no call that satisfies both, which is why this table exists.
|
|
745
|
+
|
|
717
746
|
## Chat management
|
|
718
747
|
|
|
719
748
|
Since 2.12.0 the assistant can assemble a chat, not only speak in it: create a supergroup, add and
|
|
@@ -728,15 +757,15 @@ management to those chats, `["*"]` allows every chat. A non-empty list also unlo
|
|
|
728
757
|
(the chat being created is not in any list yet). All actions honour `dryRun`, and the gate is
|
|
729
758
|
checked before the dry-run answer, so a dry run exercises the same refusals a real call would hit.
|
|
730
759
|
|
|
731
|
-
| Action | Parameters | Notes |
|
|
732
|
-
|
|
733
|
-
| `createGroup` | `title`, `about?`, `users?` | Creates a **supergroup** (megagroup) — granular admin rights, bans and ownership transfer only exist there. Initial members are invited right after creation; who could not be added is returned in `missing` |
|
|
734
|
-
| `addMembers` | `chatId`, `users` | Adds to supergroups in one call, to basic groups one by one. Ids Telegram refused (privacy settings) come back in `missing` instead of failing the call |
|
|
735
|
-
| `removeMember` | `chatId`, `user`, `ban?` | Soft kick by default — the person may be re-invited later. `ban: true` keeps them out until unbanned |
|
|
736
|
-
| `promoteAdmin` | `chatId`, `user`, `rank?`, `rights?` | Grants a deliberate default set (change info, delete messages, ban, invite, pin, calls, topics). `addAdmins` and `anonymous` stay **off** unless explicitly set in `rights` |
|
|
737
|
-
| `demoteAdmin` | `chatId`, `user` | Strips every admin right |
|
|
738
|
-
| `transferOwnership` | `chatId`, `user` | Supergroups only. Requires `twoFaPassword` (below); Telegram's own rules surface as errors — see the fine print |
|
|
739
|
-
| `inviteLink` | `chatId`, `expireDate?`, `usageLimit?`, `title?`, `requestNeeded?` | The path for people whose privacy settings refuse a direct add. `expireDate` takes unix seconds or an ISO date |
|
|
760
|
+
| Action | Callable from the `message` tool as | Parameters | Notes |
|
|
761
|
+
|---|---|---|---|
|
|
762
|
+
| `createGroup` | `channel-create` | `title`, `about?`, `users?` | Creates a **supergroup** (megagroup) — granular admin rights, bans and ownership transfer only exist there. Initial members are invited right after creation; who could not be added is returned in `missing` |
|
|
763
|
+
| `addMembers` | `addParticipant` | `chatId`, `users` | Adds to supergroups in one call, to basic groups one by one. Ids Telegram refused (privacy settings) come back in `missing` instead of failing the call |
|
|
764
|
+
| `removeMember` | `kick` | `chatId`, `user`, `ban?` | Soft kick by default — the person may be re-invited later. `ban: true` keeps them out until unbanned |
|
|
765
|
+
| `promoteAdmin` | `role-add` | `chatId`, `user`, `rank?`, `rights?` | Grants a deliberate default set (change info, delete messages, ban, invite, pin, calls, topics). `addAdmins` and `anonymous` stay **off** unless explicitly set in `rights` |
|
|
766
|
+
| `demoteAdmin` | `role-remove` | `chatId`, `user` | Strips every admin right |
|
|
767
|
+
| `transferOwnership` | — (gateway RPC only) | `chatId`, `user` | Supergroups only. Requires `twoFaPassword` (below); Telegram's own rules surface as errors — see the fine print |
|
|
768
|
+
| `inviteLink` | — (gateway RPC only) | `chatId`, `expireDate?`, `usageLimit?`, `title?`, `requestNeeded?` | The path for people whose privacy settings refuse a direct add. `expireDate` takes unix seconds or an ISO date |
|
|
740
769
|
|
|
741
770
|
User references in `users`/`user` are `@username` or a numeric Telegram id. A `@username` always
|
|
742
771
|
resolves; a bare numeric id only when the account has already seen the user (shared chat, dialog,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rememberAccount = rememberAccount;
|
|
4
|
+
exports.forgetAccount = forgetAccount;
|
|
5
|
+
exports.sendScopeFor = sendScopeFor;
|
|
6
|
+
exports.operatorIdsFor = operatorIdsFor;
|
|
7
|
+
exports.requireRuntime = requireRuntime;
|
|
8
|
+
const records = new Map();
|
|
9
|
+
function rememberAccount(accountId, record) {
|
|
10
|
+
records.set(accountId, { sendChats: record.sendChats, operatorIds: [...record.operatorIds] });
|
|
11
|
+
}
|
|
12
|
+
function forgetAccount(accountId) {
|
|
13
|
+
records.delete(accountId);
|
|
14
|
+
}
|
|
15
|
+
function sendScopeFor(accountId) {
|
|
16
|
+
return records.get(accountId)?.sendChats;
|
|
17
|
+
}
|
|
18
|
+
function operatorIdsFor(accountId) {
|
|
19
|
+
return records.get(accountId)?.operatorIds ?? [];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The connected runtime for an account, or a refusal naming it.
|
|
23
|
+
*
|
|
24
|
+
* One helper instead of the copies of this three-liner that used to sit in
|
|
25
|
+
* each dispatch branch (A6-11 removed six; D2-11 the remaining seven).
|
|
26
|
+
*/
|
|
27
|
+
function requireRuntime(runtimes, accountId) {
|
|
28
|
+
const gram = runtimes.get(accountId);
|
|
29
|
+
if (!gram) {
|
|
30
|
+
throw new Error(`clawgram: runtime not found for account ${accountId}`);
|
|
31
|
+
}
|
|
32
|
+
return gram;
|
|
33
|
+
}
|
package/dist/channel.js
CHANGED
|
@@ -55,15 +55,14 @@ const fetch_media_1 = require("./fetch-media");
|
|
|
55
55
|
const channel_runtime_1 = require("openclaw/plugin-sdk/channel-runtime");
|
|
56
56
|
const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
|
|
57
57
|
const tool_send_1 = require("openclaw/plugin-sdk/tool-send");
|
|
58
|
-
const channel_pairing_1 = require("openclaw/plugin-sdk/channel-pairing");
|
|
59
58
|
const events_1 = require("telegram/events");
|
|
60
59
|
const gramjs_client_1 = require("./gramjs-client");
|
|
61
60
|
const history_1 = require("./history");
|
|
62
61
|
const send_scope_1 = require("./send-scope");
|
|
62
|
+
const account_registry_1 = require("./account-registry");
|
|
63
63
|
const joins_1 = require("./joins");
|
|
64
64
|
const reactions_1 = require("./reactions");
|
|
65
65
|
const manage_1 = require("./manage");
|
|
66
|
-
const system_notice_1 = require("./system-notice");
|
|
67
66
|
const state_dir_1 = require("./state-dir");
|
|
68
67
|
const chat_info_1 = require("./chat-info");
|
|
69
68
|
const topics_1 = require("./topics");
|
|
@@ -76,6 +75,12 @@ const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
|
|
|
76
75
|
const helpers_1 = require("./helpers");
|
|
77
76
|
const proxy_config_1 = require("./proxy-config");
|
|
78
77
|
const constants_1 = require("./constants");
|
|
78
|
+
const actions_1 = require("./actions");
|
|
79
|
+
Object.defineProperty(exports, "CORE_ACTION_SYNONYMS", { enumerable: true, get: function () { return actions_1.CORE_ACTION_SYNONYMS; } });
|
|
80
|
+
Object.defineProperty(exports, "canonicalAction", { enumerable: true, get: function () { return actions_1.canonicalAction; } });
|
|
81
|
+
const outbound_1 = require("./outbound");
|
|
82
|
+
const inbound_pipeline_1 = require("./inbound-pipeline");
|
|
83
|
+
const attachments_1 = require("./attachments");
|
|
79
84
|
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
80
85
|
/**
|
|
81
86
|
* Read scope as configured for the account. Left `undefined` when the key is
|
|
@@ -83,20 +88,11 @@ const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
|
83
88
|
* the first means no restriction, the second denies everything.
|
|
84
89
|
*/
|
|
85
90
|
function readAccountReadChats(account) {
|
|
86
|
-
|
|
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
|
+
return (0, history_1.normalizeScopeList)(account?.readChats);
|
|
91
92
|
}
|
|
92
93
|
function resolveAccountReadChats(cfg, accountId) {
|
|
93
94
|
return readAccountReadChats(cfg?.channels?.["clawgram"]?.accounts?.[accountId]);
|
|
94
95
|
}
|
|
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
96
|
/**
|
|
101
97
|
* Хэндл в `allowFrom` — обещание, которое Telegram не держит.
|
|
102
98
|
*
|
|
@@ -123,16 +119,19 @@ function warnAboutHandleAllowlistEntries(cfg, accountId) {
|
|
|
123
119
|
why: "a released handle can be taken by someone else; numeric ids do not change hands",
|
|
124
120
|
});
|
|
125
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Outbound scope as configured. Handed to `isChatSendable` raw: an absent
|
|
124
|
+
* value means "unrestricted" and an empty list means "deny", and only the
|
|
125
|
+
* raw value tells those apart — same shape as `readChats`.
|
|
126
|
+
*/
|
|
126
127
|
function resolveAccountSendChats(cfg, accountId) {
|
|
127
128
|
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.sendChats;
|
|
128
129
|
}
|
|
129
130
|
/** One refusal for every outbound action, so the three read the same. */
|
|
130
131
|
function refuseOutboundOutsideScope(action, accountId, target) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
actionLog.warn(`clawgram ${action} refused: ${reason}`, { accountId, ...(phone ? { targetKind: "phone" } : { target }) });
|
|
135
|
-
throw new Error(`clawgram: not-allowed-chat ${target}`);
|
|
132
|
+
const refusal = (0, send_scope_1.describeSendRefusal)(target);
|
|
133
|
+
actionLog.warn(`clawgram ${action} refused: ${refusal.reason}`, { accountId, ...refusal.logFields });
|
|
134
|
+
throw refusal.error;
|
|
136
135
|
}
|
|
137
136
|
/**
|
|
138
137
|
* Management scope as configured. Handed to `isChatManageable` raw: unlike
|
|
@@ -170,31 +169,8 @@ function resolveAccountManageChats(cfg, accountId) {
|
|
|
170
169
|
}
|
|
171
170
|
/** Same normalization `readChats` gets, for the resolved-account copy. */
|
|
172
171
|
function readAccountManageChats(account) {
|
|
173
|
-
|
|
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);
|
|
172
|
+
return (0, history_1.normalizeScopeList)(account?.manageChats);
|
|
178
173
|
}
|
|
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
174
|
const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
199
175
|
const resolveRuntimeAccountId = (cfg, preferred) => {
|
|
200
176
|
const configured = (0, helpers_1.resolveConfiguredAccountId)(cfg, preferred);
|
|
@@ -206,20 +182,8 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
206
182
|
}
|
|
207
183
|
return configured ?? runtimes.keys().next().value;
|
|
208
184
|
};
|
|
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
|
-
};
|
|
185
|
+
/** The connected runtime for an account, or a refusal naming it (A6-11, D2-11). */
|
|
186
|
+
const requireRuntimeFor = (id) => (0, account_registry_1.requireRuntime)(runtimes, id);
|
|
223
187
|
return {
|
|
224
188
|
id: "clawgram",
|
|
225
189
|
meta: {
|
|
@@ -364,18 +328,14 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
364
328
|
const gram = new gramjs_client_1.GramJsClientManager(resolvedAccount);
|
|
365
329
|
await gram.start();
|
|
366
330
|
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,
|
|
331
|
+
// Что `outbound.*` должен знать об аккаунте без конфига: область
|
|
332
|
+
// отправки (A5-12) и операторы (A5-11). Одна запись, снимается при
|
|
333
|
+
// остановке аккаунта (D2-11).
|
|
334
|
+
(0, account_registry_1.rememberAccount)(accountId, {
|
|
335
|
+
sendChats: resolveAccountSendChats(cfg, accountId),
|
|
336
|
+
operatorIds: resolveAccountOperatorIds(cfg, accountId),
|
|
378
337
|
});
|
|
338
|
+
warnAboutHandleAllowlistEntries(cfg, accountId);
|
|
379
339
|
const me = await gram.getMe();
|
|
380
340
|
const selfId = me?.id ? String(me.id) : undefined;
|
|
381
341
|
const selfUsername = (0, helpers_1.resolveActiveUsername)(me);
|
|
@@ -394,7 +354,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
394
354
|
const client = gram.getClient();
|
|
395
355
|
const eventBuilder = new events_1.NewMessage({});
|
|
396
356
|
const eventHandler = async (event) => (0, inbound_pipeline_1.handleInboundEvent)(event, {
|
|
397
|
-
accountId, cfg, channelRuntime, client, gram, log,
|
|
357
|
+
accountId, cfg, channelRuntime, client, gram, log,
|
|
398
358
|
pluginRuntime, runtimes, selfId, selfLabel, selfUsername,
|
|
399
359
|
});
|
|
400
360
|
client.addEventHandler(eventHandler, eventBuilder);
|
|
@@ -431,6 +391,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
431
391
|
await (0, channel_runtime_1.waitUntilAbort)(ctx.abortSignal, async () => {
|
|
432
392
|
client.removeEventHandler(eventHandler, eventBuilder);
|
|
433
393
|
client.removeEventHandler(joinEventHandler, joinEventBuilder);
|
|
394
|
+
(0, account_registry_1.forgetAccount)(accountId);
|
|
434
395
|
const runtime = runtimes.get(accountId);
|
|
435
396
|
if (!runtime) {
|
|
436
397
|
return;
|
|
@@ -618,8 +579,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
618
579
|
// bundled channels enforce them. This one used to take `filePath`
|
|
619
580
|
// verbatim, so a path naming the secret store or the config holding
|
|
620
581
|
// `sessionString` was uploaded like any attachment.
|
|
621
|
-
mediaLocalRoots, mediaAccess, }) => {
|
|
582
|
+
mediaLocalRoots, mediaReadFile, mediaAccess, }) => {
|
|
622
583
|
const allowedMediaRoots = mediaLocalRoots ?? mediaAccess?.localRoots;
|
|
584
|
+
const readMedia = mediaReadFile ?? mediaAccess?.readFile;
|
|
623
585
|
// Core passes the flag beside `params`; callers write it inside.
|
|
624
586
|
// Both count, because a rehearsal flag that is silently ignored puts
|
|
625
587
|
// a real message in a real chat — twice, so far (2.13.1).
|
|
@@ -646,10 +608,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
646
608
|
});
|
|
647
609
|
throw new Error(`clawgram: not-allowed-chat ${listParams.target}`);
|
|
648
610
|
}
|
|
649
|
-
const listGram =
|
|
650
|
-
if (!listGram) {
|
|
651
|
-
throw new Error(`clawgram: runtime not found for account ${listAccountId}`);
|
|
652
|
-
}
|
|
611
|
+
const listGram = requireRuntimeFor(listAccountId);
|
|
653
612
|
const history = await listGram.listMessages(listParams);
|
|
654
613
|
// Metadata only. Message text is the user's correspondence and has no
|
|
655
614
|
// business in a log that is read while debugging something else.
|
|
@@ -693,10 +652,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
693
652
|
});
|
|
694
653
|
throw new Error(`clawgram: not-allowed-chat ${fetchParams.target}`);
|
|
695
654
|
}
|
|
696
|
-
const fetchGram =
|
|
697
|
-
if (!fetchGram) {
|
|
698
|
-
throw new Error(`clawgram: runtime not found for account ${fetchAccountId}`);
|
|
699
|
-
}
|
|
655
|
+
const fetchGram = requireRuntimeFor(fetchAccountId);
|
|
700
656
|
// Fetching is a read: a dry run answers for real, the same way `read`
|
|
701
657
|
// does. Nothing leaves the machine — the file lands in a temp
|
|
702
658
|
// directory this channel prunes — so a rehearsal that reported
|
|
@@ -1024,10 +980,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1024
980
|
removed: reactionParams.remove,
|
|
1025
981
|
});
|
|
1026
982
|
}
|
|
1027
|
-
const reactionGram =
|
|
1028
|
-
if (!reactionGram) {
|
|
1029
|
-
throw new Error(`clawgram: runtime not found for account ${reactionAccountId}`);
|
|
1030
|
-
}
|
|
983
|
+
const reactionGram = requireRuntimeFor(reactionAccountId);
|
|
1031
984
|
await reactionGram.sendReaction(reactionParams);
|
|
1032
985
|
return (0, core_1.jsonResult)({
|
|
1033
986
|
ok: true,
|
|
@@ -1239,13 +1192,12 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1239
1192
|
if (!(0, send_scope_1.isChatSendable)(uploadTo, resolveAccountSendChats(cfg, uploadAccountId))) {
|
|
1240
1193
|
refuseOutboundOutsideScope("upload-file", uploadAccountId, uploadTo);
|
|
1241
1194
|
}
|
|
1242
|
-
|
|
1243
|
-
if (!file) {
|
|
1195
|
+
if (!attachedFile) {
|
|
1244
1196
|
throw new Error("clawgram: upload-file requires filePath, path, media, or mediaUrl");
|
|
1245
1197
|
}
|
|
1246
1198
|
// Before anything else about the message is considered: an
|
|
1247
1199
|
// out-of-scope path is refused, not sent and then regretted.
|
|
1248
|
-
(0, media_1.assertLocalMediaWithinRoots)(
|
|
1200
|
+
(0, media_1.assertLocalMediaWithinRoots)(attachedFile, allowedMediaRoots);
|
|
1249
1201
|
const captionText = (0, helpers_1.readMessageText)(params) || ((0, param_readers_1.readStringParam)(params, "caption") ?? "");
|
|
1250
1202
|
// A caption is optional, but the silent-reply sentinel must never
|
|
1251
1203
|
// reach Telegram as one — same reasoning as the `send` path below.
|
|
@@ -1272,10 +1224,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1272
1224
|
accountId: uploadAccountId,
|
|
1273
1225
|
});
|
|
1274
1226
|
}
|
|
1275
|
-
const uploadGram =
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1227
|
+
const uploadGram = requireRuntimeFor(uploadAccountId);
|
|
1228
|
+
// Read last, through core's scoped reader when it gave one: a dry
|
|
1229
|
+
// run or a refusal above must not open the file.
|
|
1230
|
+
const file = await (0, media_1.loadOutboundMedia)(attachedFile, allowedMediaRoots, readMedia);
|
|
1279
1231
|
const uploaded = await uploadGram.sendMedia({
|
|
1280
1232
|
target: uploadTo,
|
|
1281
1233
|
file,
|
|
@@ -1413,10 +1365,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1413
1365
|
accountId: resolvedAccountId,
|
|
1414
1366
|
});
|
|
1415
1367
|
}
|
|
1416
|
-
const gram =
|
|
1417
|
-
if (!gram) {
|
|
1418
|
-
throw new Error(`clawgram: runtime not found for account ${resolvedAccountId}`);
|
|
1419
|
-
}
|
|
1368
|
+
const gram = requireRuntimeFor(resolvedAccountId);
|
|
1420
1369
|
const sent = await gram.sendText({
|
|
1421
1370
|
target: to,
|
|
1422
1371
|
text,
|
package/dist/gramjs-client.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.buildVoiceNoteParams = buildVoiceNoteParams;
|
|
|
6
6
|
const chunk_1 = require("./chunk");
|
|
7
7
|
const telegram_1 = require("telegram");
|
|
8
8
|
const sessions_1 = require("telegram/sessions");
|
|
9
|
+
const uploads_1 = require("telegram/client/uploads");
|
|
9
10
|
// Deep import, but the documented one: GramJS ships its SRP helper here and
|
|
10
11
|
// the package has no `exports` field to forbid it.
|
|
11
12
|
const Password_1 = require("telegram/Password");
|
|
@@ -63,29 +64,6 @@ function uniqueCandidates(values) {
|
|
|
63
64
|
}
|
|
64
65
|
return result;
|
|
65
66
|
}
|
|
66
|
-
function parseTargetWithThread(rawTarget) {
|
|
67
|
-
const raw = rawTarget.trim();
|
|
68
|
-
const topicMatch = /^(.+?):topic:(\d+)$/.exec(raw);
|
|
69
|
-
if (topicMatch) {
|
|
70
|
-
return {
|
|
71
|
-
raw,
|
|
72
|
-
chatId: topicMatch[1],
|
|
73
|
-
messageThreadId: Number.parseInt(topicMatch[2], 10),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
const colonMatch = /^(.+):(\d+)$/.exec(raw);
|
|
77
|
-
if (colonMatch && /^-?\d+$/.test(colonMatch[1])) {
|
|
78
|
-
return {
|
|
79
|
-
raw,
|
|
80
|
-
chatId: colonMatch[1],
|
|
81
|
-
messageThreadId: Number.parseInt(colonMatch[2], 10),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
raw,
|
|
86
|
-
chatId: raw,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
67
|
/**
|
|
90
68
|
* Membership query for `getParticipants`.
|
|
91
69
|
*
|
|
@@ -358,7 +336,7 @@ class GramJsClientManager {
|
|
|
358
336
|
peer: entity
|
|
359
337
|
};
|
|
360
338
|
}
|
|
361
|
-
const parsedTarget = parseTargetWithThread(rawTarget);
|
|
339
|
+
const parsedTarget = (0, history_1.parseTargetWithThread)(rawTarget);
|
|
362
340
|
const raw = parsedTarget.raw;
|
|
363
341
|
const chatLookupTarget = parsedTarget.chatId;
|
|
364
342
|
const kind = options?.kind;
|
|
@@ -781,7 +759,11 @@ class GramJsClientManager {
|
|
|
781
759
|
return sent;
|
|
782
760
|
}
|
|
783
761
|
return this.client.sendFile(resolved.peer, {
|
|
784
|
-
file
|
|
762
|
+
// Bytes core read through its scoped reader keep the file's own name;
|
|
763
|
+
// a bare Buffer would reach Telegram as "unnamed" (B5-14).
|
|
764
|
+
file: typeof args.file === "string"
|
|
765
|
+
? args.file
|
|
766
|
+
: new uploads_1.CustomFile(args.file.fileName, args.file.buffer.length, "", args.file.buffer),
|
|
785
767
|
// Captions are agent prose too — the outbound path sends `caption ??
|
|
786
768
|
// text` — so they render exactly like sendText does. Before 2.15.0
|
|
787
769
|
// captions carried no mode at all, which meant GramJS's default
|
package/dist/helpers.js
CHANGED
|
@@ -834,8 +834,11 @@ function normalizeParseMode(raw) {
|
|
|
834
834
|
* raw markup.
|
|
835
835
|
*/
|
|
836
836
|
function resolveReplyParseMode(cfg, accountId) {
|
|
837
|
-
|
|
838
|
-
|
|
837
|
+
// Account level only: the schema has never allowed `replyParseMode` on the
|
|
838
|
+
// channel itself, so the old fallback to `channels.clawgram.replyParseMode`
|
|
839
|
+
// read a key `openclaw config validate` rejects — a setting that could not
|
|
840
|
+
// exist was read, and a test pinned it (audit B5-10).
|
|
841
|
+
const account = cfg?.channels?.["clawgram"]?.accounts?.[accountId];
|
|
839
842
|
return normalizeParseMode(account?.replyParseMode);
|
|
840
843
|
}
|
|
841
844
|
/**
|
package/dist/history.js
CHANGED
|
@@ -20,6 +20,8 @@ exports.normalizeParticipants = normalizeParticipants;
|
|
|
20
20
|
exports.parseListParticipantsParams = parseListParticipantsParams;
|
|
21
21
|
exports.buildHistoryQuery = buildHistoryQuery;
|
|
22
22
|
exports.normalizeChatKey = normalizeChatKey;
|
|
23
|
+
exports.normalizeScopeList = normalizeScopeList;
|
|
24
|
+
exports.parseTargetWithThread = parseTargetWithThread;
|
|
23
25
|
exports.chatKeyCandidates = chatKeyCandidates;
|
|
24
26
|
exports.isChatReadable = isChatReadable;
|
|
25
27
|
exports.isWithinWindow = isWithinWindow;
|
|
@@ -234,6 +236,20 @@ function buildHistoryQuery(args) {
|
|
|
234
236
|
function normalizeChatKey(value) {
|
|
235
237
|
return String(value ?? "").trim().replace(/^@/, "").toLowerCase();
|
|
236
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* A configured chat scope (`readChats`, `sendChats`, `manageChats`) as a list
|
|
241
|
+
* of chat keys — or `undefined` when the key is absent, because every gate
|
|
242
|
+
* tells "not configured" from "configured empty" by that difference.
|
|
243
|
+
*
|
|
244
|
+
* Four copies of this normalizer used to live in three files, two of them
|
|
245
|
+
* trimming only and two lowercasing, so the same entry could pass one gate
|
|
246
|
+
* and fail another (audit B5-13). One now.
|
|
247
|
+
*/
|
|
248
|
+
function normalizeScopeList(raw) {
|
|
249
|
+
if (raw === undefined || raw === null)
|
|
250
|
+
return undefined;
|
|
251
|
+
return (Array.isArray(raw) ? raw : [raw]).map(normalizeChatKey).filter(Boolean);
|
|
252
|
+
}
|
|
237
253
|
/**
|
|
238
254
|
* Все написания одной цели, по которым её ищут в списке доступа.
|
|
239
255
|
*
|
|
@@ -247,11 +263,44 @@ function normalizeChatKey(value) {
|
|
|
247
263
|
* `-1001234:topic:5` сегодня работает как область в одну тему, и сведение
|
|
248
264
|
* всего к чату молча расширило бы её на весь чат.
|
|
249
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* One target address, split into the chat and the forum topic it may name.
|
|
268
|
+
*
|
|
269
|
+
* Two spellings carry a topic: `-1001234:topic:5` and the short `-1001234:5`
|
|
270
|
+
* (a numeric chat, a colon, a number). This is the parser the resolver in
|
|
271
|
+
* `gramjs-client` uses, and since B5-08 the only one: the gates used to strip
|
|
272
|
+
* `:topic:N` with a regex of their own and did not know the short form, so
|
|
273
|
+
* `-1001234:5` passed the resolver as a topic of a listed chat and failed the
|
|
274
|
+
* gate as an unknown one.
|
|
275
|
+
*/
|
|
276
|
+
function parseTargetWithThread(rawTarget) {
|
|
277
|
+
const raw = rawTarget.trim();
|
|
278
|
+
const topicMatch = /^(.+?):topic:(\d+)$/.exec(raw);
|
|
279
|
+
if (topicMatch) {
|
|
280
|
+
return {
|
|
281
|
+
raw,
|
|
282
|
+
chatId: topicMatch[1],
|
|
283
|
+
messageThreadId: Number.parseInt(topicMatch[2], 10),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const colonMatch = /^(.+):(\d+)$/.exec(raw);
|
|
287
|
+
if (colonMatch && /^-?\d+$/.test(colonMatch[1])) {
|
|
288
|
+
return {
|
|
289
|
+
raw,
|
|
290
|
+
chatId: colonMatch[1],
|
|
291
|
+
messageThreadId: Number.parseInt(colonMatch[2], 10),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
raw,
|
|
296
|
+
chatId: raw,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
250
299
|
function chatKeyCandidates(target) {
|
|
251
300
|
const raw = String(target ?? "").trim();
|
|
252
301
|
const withoutChannel = raw.replace(/^(?:clawgram|tguserbot|telegram|tg):/i, "");
|
|
253
302
|
const withoutKind = withoutChannel.replace(/^(?:user|channel|group|conversation|room|dm):/i, "");
|
|
254
|
-
const chatOnly = withoutKind.
|
|
303
|
+
const chatOnly = parseTargetWithThread(withoutKind).chatId;
|
|
255
304
|
const candidates = [raw, withoutKind, chatOnly]
|
|
256
305
|
.map(normalizeChatKey)
|
|
257
306
|
.filter(Boolean);
|
|
@@ -280,11 +329,9 @@ function isChatReadable(target, readChats) {
|
|
|
280
329
|
const candidates = chatKeyCandidates(target);
|
|
281
330
|
if (candidates.includes(constants_1.TELEGRAM_SERVICE_CHAT_ID))
|
|
282
331
|
return false;
|
|
283
|
-
|
|
332
|
+
const entries = normalizeScopeList(readChats);
|
|
333
|
+
if (entries === undefined)
|
|
284
334
|
return true;
|
|
285
|
-
const entries = (Array.isArray(readChats) ? readChats : [readChats])
|
|
286
|
-
.map(normalizeChatKey)
|
|
287
|
-
.filter(Boolean);
|
|
288
335
|
// An empty list is a configured empty list — deny, rather than silently
|
|
289
336
|
// reading everything because someone left brackets behind.
|
|
290
337
|
if (entries.length === 0)
|
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;
|
|
@@ -326,6 +373,12 @@ async function handleInboundEvent(event, ctx) {
|
|
|
326
373
|
// the answer — and by the same resolver `resolveAccount` uses, so the
|
|
327
374
|
// gate applied here is the one the account was started with.
|
|
328
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).
|
|
329
382
|
const dmPolicy = "open";
|
|
330
383
|
if (normalized.chatType === "group") {
|
|
331
384
|
const groupConfig = inboundGroupConfig;
|
|
@@ -555,38 +608,11 @@ async function handleInboundEvent(event, ctx) {
|
|
|
555
608
|
payloadTextLength: outboundText.length,
|
|
556
609
|
payloadReplyToId: payload.replyToId ?? null,
|
|
557
610
|
});
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
// The agent may decline to answer by returning the shared
|
|
562
|
-
// silent token. Drop it before addressing: otherwise the
|
|
563
|
-
// reply-address prefix turns it into a visible message.
|
|
564
|
-
const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
|
|
565
|
-
if (!visibleText) {
|
|
566
|
-
log?.info?.("clawgram suppressing silent group reply", {
|
|
567
|
-
accountId,
|
|
568
|
-
chatId: normalized.chatId,
|
|
569
|
-
messageId: normalized.messageId,
|
|
570
|
-
});
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
// Ядро подклеивает свою телеметрию к полезной нагрузке
|
|
574
|
-
// хода, и сюда она приходит тем же путём, что ответ.
|
|
575
|
-
// Проверка стояла только в `outbound.sendText`, то есть
|
|
576
|
-
// класс инцидента 30.08–01.09 был закрыт для рассылок и
|
|
577
|
-
// открыт для обычного ответа на упоминание (A5-10).
|
|
578
|
-
const groupNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
|
|
579
|
-
targetKind: "group",
|
|
580
|
-
text: visibleText,
|
|
611
|
+
const visibleText = visibleReplyText({
|
|
612
|
+
text: outboundText, kind: "group", where: "group reply",
|
|
613
|
+
accountId, chatId: normalized.chatId, messageId: normalized.messageId, log,
|
|
581
614
|
});
|
|
582
|
-
if (
|
|
583
|
-
log?.warn?.("clawgram suppressing system notice in group reply", {
|
|
584
|
-
accountId,
|
|
585
|
-
chatId: normalized.chatId,
|
|
586
|
-
messageId: normalized.messageId,
|
|
587
|
-
noticeKind: groupNotice,
|
|
588
|
-
textLength: visibleText.length,
|
|
589
|
-
});
|
|
615
|
+
if (!visibleText) {
|
|
590
616
|
return;
|
|
591
617
|
}
|
|
592
618
|
const replyToMessageId = payload.replyToId ? Number(payload.replyToId) : Number(normalized.messageId);
|
|
@@ -647,19 +673,10 @@ async function handleInboundEvent(event, ctx) {
|
|
|
647
673
|
: "";
|
|
648
674
|
// Тот же фильтр и здесь: последняя реплика в стенограмме
|
|
649
675
|
// вполне может оказаться именно уведомлением об ошибке.
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
:
|
|
653
|
-
|
|
654
|
-
log?.warn?.("clawgram suppressing system notice in transcript fallback", {
|
|
655
|
-
accountId,
|
|
656
|
-
chatId: normalized.chatId,
|
|
657
|
-
messageId: normalized.messageId,
|
|
658
|
-
noticeKind: fallbackNotice,
|
|
659
|
-
textLength: rawFallback.length,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
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
|
+
}) ?? "";
|
|
663
680
|
if (!visibleFallbackText) {
|
|
664
681
|
if (fallbackText) {
|
|
665
682
|
log?.info?.("clawgram skipping silent transcript fallback", {
|
|
@@ -769,7 +786,6 @@ async function handleInboundEvent(event, ctx) {
|
|
|
769
786
|
senderId,
|
|
770
787
|
senderUsername,
|
|
771
788
|
}),
|
|
772
|
-
readStoreAllowFrom: pairing.readStoreForDmPolicy,
|
|
773
789
|
});
|
|
774
790
|
if (access.access.decision === "block") {
|
|
775
791
|
log?.info?.("clawgram blocking inbound direct message", {
|
|
@@ -782,36 +798,6 @@ async function handleInboundEvent(event, ctx) {
|
|
|
782
798
|
});
|
|
783
799
|
return;
|
|
784
800
|
}
|
|
785
|
-
if (access.access.decision === "pairing") {
|
|
786
|
-
await pairing.issueChallenge({
|
|
787
|
-
senderId,
|
|
788
|
-
senderIdLine: `Your Telegram user id: ${senderId}`,
|
|
789
|
-
meta: {
|
|
790
|
-
username: normalized.senderUsername,
|
|
791
|
-
name: normalized.senderDisplay,
|
|
792
|
-
},
|
|
793
|
-
sendPairingReply: async (pairingText) => {
|
|
794
|
-
await sendTextToConversation({
|
|
795
|
-
text: pairingText,
|
|
796
|
-
});
|
|
797
|
-
},
|
|
798
|
-
onReplyError: (err) => {
|
|
799
|
-
log?.info?.("clawgram pairing reply failed", {
|
|
800
|
-
accountId,
|
|
801
|
-
chatId: normalized.chatId,
|
|
802
|
-
senderId,
|
|
803
|
-
error: String(err),
|
|
804
|
-
});
|
|
805
|
-
},
|
|
806
|
-
});
|
|
807
|
-
log?.info?.("clawgram pairing required for inbound direct message", {
|
|
808
|
-
accountId,
|
|
809
|
-
chatId: normalized.chatId,
|
|
810
|
-
messageId: normalized.messageId,
|
|
811
|
-
senderId,
|
|
812
|
-
});
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
801
|
// Same fetch as the group path. In a DM the parent is as often
|
|
816
802
|
// the agent's own message as the person's — the owner answers a
|
|
817
803
|
// notice she sent — and neither text is available any other way.
|
|
@@ -852,37 +838,13 @@ async function handleInboundEvent(event, ctx) {
|
|
|
852
838
|
NativeChannelId: normalized.chatId,
|
|
853
839
|
},
|
|
854
840
|
deliver: async (payload) => {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (!visibleText) {
|
|
861
|
-
log?.info?.("clawgram suppressing silent direct reply", {
|
|
862
|
-
accountId,
|
|
863
|
-
chatId: normalized.chatId,
|
|
864
|
-
messageId: normalized.messageId,
|
|
865
|
-
});
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
// Тот же фильтр, что у группового ответа и у outbound.sendText:
|
|
869
|
-
// личный ответ идёт третьим путём, и закрытие A5-11 его не
|
|
870
|
-
// покрывало — «⚠️ 🛠️ Bash failed: cat /opt/openclaw-secrets/…»
|
|
871
|
-
// уходил любому из allowFrom, чей ход уронил инструмент (B5-01).
|
|
872
|
-
const directNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
|
|
873
|
-
targetKind: "user",
|
|
874
|
-
text: visibleText,
|
|
875
|
-
to: normalized.chatId,
|
|
876
|
-
operatorIds: (0, system_notice_1.operatorIdsFor)(accountId),
|
|
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,
|
|
877
846
|
});
|
|
878
|
-
if (
|
|
879
|
-
log?.warn?.("clawgram suppressing system notice in direct reply", {
|
|
880
|
-
accountId,
|
|
881
|
-
chatId: normalized.chatId,
|
|
882
|
-
messageId: normalized.messageId,
|
|
883
|
-
noticeKind: directNotice,
|
|
884
|
-
textLength: visibleText.length,
|
|
885
|
-
});
|
|
847
|
+
if (!visibleText) {
|
|
886
848
|
return;
|
|
887
849
|
}
|
|
888
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
|
}
|
|
@@ -94,11 +93,12 @@ function createOutbound(runtimes) {
|
|
|
94
93
|
// handleAction, где барьер уже стоял: третий из трёх исходящих путей
|
|
95
94
|
// был открыт для любого адресата и телефонного номера (D2-01, A5-12).
|
|
96
95
|
const scopedTarget = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
97
|
-
if (!(0, send_scope_1.isChatSendable)(scopedTarget, (0,
|
|
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
98
|
actionLog.warn("clawgram outbound sendText refused", {
|
|
99
99
|
accountId: ctx.accountId,
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
reason: refusal.reason,
|
|
101
|
+
...refusal.logFields,
|
|
102
102
|
});
|
|
103
103
|
return { skipped: "not-allowed" };
|
|
104
104
|
}
|
|
@@ -111,7 +111,7 @@ function createOutbound(runtimes) {
|
|
|
111
111
|
targetKind: (0, helpers_1.inferOutboundTargetKind)(ctx.to),
|
|
112
112
|
text: ctx.text,
|
|
113
113
|
to: ctx.to,
|
|
114
|
-
operatorIds: (0,
|
|
114
|
+
operatorIds: (0, account_registry_1.operatorIdsFor)(ctx.accountId),
|
|
115
115
|
});
|
|
116
116
|
if (suppressedNotice) {
|
|
117
117
|
actionLog.warn("clawgram suppressing system notice in group", {
|
|
@@ -122,10 +122,7 @@ function createOutbound(runtimes) {
|
|
|
122
122
|
});
|
|
123
123
|
return { skipped: "system-notice" };
|
|
124
124
|
}
|
|
125
|
-
const gram =
|
|
126
|
-
if (!gram) {
|
|
127
|
-
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
128
|
-
}
|
|
125
|
+
const gram = (0, account_registry_1.requireRuntime)(runtimes, ctx.accountId);
|
|
129
126
|
// The agent already answered this message with its own `send`, and this
|
|
130
127
|
// is core delivering the same turn's final text. Two messages for one
|
|
131
128
|
// answer is how 2026-08-10 read in a work chat: every request reported
|
|
@@ -177,10 +174,7 @@ function createOutbound(runtimes) {
|
|
|
177
174
|
};
|
|
178
175
|
},
|
|
179
176
|
async sendMedia(ctx) {
|
|
180
|
-
const gram =
|
|
181
|
-
if (!gram) {
|
|
182
|
-
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
|
183
|
-
}
|
|
177
|
+
const gram = (0, account_registry_1.requireRuntime)(runtimes, ctx.accountId);
|
|
184
178
|
// Same rule as the action path: a local file outside the declared
|
|
185
179
|
// roots is refused before anything is uploaded.
|
|
186
180
|
const outboundRoots = ctx.mediaLocalRoots ?? ctx.mediaAccess?.localRoots;
|
|
@@ -197,8 +191,8 @@ function createOutbound(runtimes) {
|
|
|
197
191
|
hasCaption: Boolean(ctx.caption),
|
|
198
192
|
asVoice: ctx.audioAsVoice === true,
|
|
199
193
|
});
|
|
200
|
-
const
|
|
201
|
-
if (!
|
|
194
|
+
const named = ctx.filePath ?? ctx.mediaUrl;
|
|
195
|
+
if (!named) {
|
|
202
196
|
throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
|
|
203
197
|
}
|
|
204
198
|
// Ниже — проверки, которые у `sendText` были, а здесь не было ни
|
|
@@ -207,11 +201,12 @@ function createOutbound(runtimes) {
|
|
|
207
201
|
const mediaTarget = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
|
|
208
202
|
// Область отправки: файл наружу — такое же исходящее, как текст.
|
|
209
203
|
// `resolveTarget` ядро зовёт не на каждом пути, поэтому проверяем и тут.
|
|
210
|
-
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);
|
|
211
206
|
actionLog.warn("clawgram outbound sendMedia refused", {
|
|
212
207
|
accountId: ctx.accountId,
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
reason: refusal.reason,
|
|
209
|
+
...refusal.logFields,
|
|
215
210
|
});
|
|
216
211
|
return { skipped: "not-allowed" };
|
|
217
212
|
}
|
|
@@ -244,6 +239,9 @@ function createOutbound(runtimes) {
|
|
|
244
239
|
// exactly how a synthesized group reply died on 2026-08-08, silently
|
|
245
240
|
// enough that the transcript fallback posted it as raw text instead.
|
|
246
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);
|
|
247
245
|
const sent = await gram.sendMedia({
|
|
248
246
|
target,
|
|
249
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/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,
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawgram",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"verify:proxy": "node scripts/verify/gramjs-wiring.cjs && node scripts/verify/auth-preserves-proxy.cjs",
|
|
8
|
+
"verify:actions": "node scripts/verify/action-probe.cjs",
|
|
8
9
|
"build": "tsc -p tsconfig.json",
|
|
9
10
|
"build:test": "tsc -p tsconfig.test.json",
|
|
10
11
|
"test": "npm run build:test && node test/ensure-compiled.mjs && node --test \"dist-test/test/*.test.js\"",
|