clawgram 2.15.0 → 2.17.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 +65 -0
- package/dist/channel.js +108 -1
- package/dist/chat-info.js +4 -1
- package/dist/dialogs.js +88 -0
- package/dist/gramjs-client.js +67 -29
- package/dist/group-tool-policy.js +36 -0
- package/dist/helpers.js +39 -0
- package/dist/history.js +62 -2
- package/dist/normalize.js +5 -5
- package/dist/topics.js +84 -0
- package/openclaw.plugin.json +151 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -183,6 +183,12 @@ openclaw gateway restart
|
|
|
183
183
|
openclaw gateway restart
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
One restart after installation is enough. From 2.17.0 the plugin declares
|
|
187
|
+
`channels.clawgram` as a hot-reloadable prefix, so later edits under it —
|
|
188
|
+
`allowFrom`, `groups`, `readChats`, proxy — are picked up by the Gateway's
|
|
189
|
+
config watcher and restart only this channel (a few seconds of MTProto
|
|
190
|
+
reconnect), not the whole Gateway.
|
|
191
|
+
|
|
186
192
|
|
|
187
193
|
## Configuration Reference
|
|
188
194
|
|
|
@@ -238,6 +244,65 @@ Group config fields:
|
|
|
238
244
|
| `enabled` | boolean | `true` | Enables or disables replies in the group |
|
|
239
245
|
| `groupPolicy` | `"open"` \| `"mention"` | `"mention"` | `open` replies to any group message, `mention` only on @mention or reply-to-self |
|
|
240
246
|
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames inside that group |
|
|
247
|
+
| `tools` | object | unset | `{ allow?, alsoAllow?, deny? }` — tool policy for this group; see [Per-group tools, skills and system prompt](#per-group-tools-skills-and-system-prompt) |
|
|
248
|
+
| `toolsBySender` | object | unset | Per-sender tool policy inside this group, keys `id:<id>`, `username:<handle>`, `name:<display>` or `*` |
|
|
249
|
+
| `skills` | string[] | unset | Skill allowlist for this group; `[]` = no skills here, unset = the agent's skills |
|
|
250
|
+
| `systemPrompt` | string | unset | Trusted prompt block appended for messages from this group |
|
|
251
|
+
|
|
252
|
+
### Per-group tools, skills and system prompt
|
|
253
|
+
|
|
254
|
+
Since 2.17.0 a group entry can narrow what the assistant does *in that chat*
|
|
255
|
+
without touching the agent as a whole. The keys mirror the bundled Telegram
|
|
256
|
+
channel's group config, and `*` works as the default group for them too.
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
"groups": {
|
|
260
|
+
"-1001234567890": {
|
|
261
|
+
"groupPolicy": "mention",
|
|
262
|
+
"systemPrompt": "This chat is project BRO. Other projects are out of scope here.",
|
|
263
|
+
"skills": ["pm-standup", "pm-jira"],
|
|
264
|
+
"tools": { "deny": ["browser", "cron"] },
|
|
265
|
+
"toolsBySender": { "id:123456789": { "alsoAllow": ["cron"] } }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Key | Effect |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `systemPrompt` | Appended to the system prompt as a trusted block for turns from this group — the place to say what the chat is about and what stays out of it. |
|
|
273
|
+
| `skills` | Skill allowlist for turns from this group. Omit to inherit the agent's skills; `[]` means no skills in this chat. |
|
|
274
|
+
| `tools` / `toolsBySender` | Tool policy resolved by OpenClaw core for this group (`toolsBySender` wins for a matching sender). Keys use core's typed grammar: `id:`, `username:`, `name:`, `channel:clawgram:<id>` or `*`. |
|
|
275
|
+
|
|
276
|
+
**Limits — read before relying on `tools`.** The policy governs OpenClaw's
|
|
277
|
+
gateway tools (`message`, `sessions_*`, `cron`, `memory_*`, …). Under CLI
|
|
278
|
+
backends such as `claude-cli` those reach the model through the loopback MCP
|
|
279
|
+
tool list and are filtered per group; the backend's own native tools —
|
|
280
|
+
`exec`, `read`, `write`, `edit`, `apply_patch`, `process` — are governed by
|
|
281
|
+
the agent's exec policy, not by the group. If a chat must not have a shell at
|
|
282
|
+
all, bind it to a separate agent without one; the routing is core's, and the
|
|
283
|
+
peer id carries the account prefix this channel uses:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
"agents": {
|
|
287
|
+
"list": [
|
|
288
|
+
{ "id": "main", "default": true, "workspace": "~/.openclaw/workspace" },
|
|
289
|
+
{
|
|
290
|
+
"id": "without-hands",
|
|
291
|
+
"workspace": "~/.openclaw/workspace-without-hands",
|
|
292
|
+
"model": "anthropic/claude-sonnet-5",
|
|
293
|
+
"tools": {
|
|
294
|
+
"deny": ["exec", "read", "write", "edit", "apply_patch", "process", "browser", "cron"],
|
|
295
|
+
"message": { "actions": { "allow": ["send"] }, "crossContext": { "allowWithinProvider": false } }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
"bindings": [
|
|
301
|
+
{ "agentId": "without-hands", "match": { "channel": "clawgram", "peer": { "kind": "group", "id": "default:-1001234567890" } } }
|
|
302
|
+
]
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Nothing above changes for a group that does not set these keys.
|
|
241
306
|
|
|
242
307
|
### Message formatting
|
|
243
308
|
|
package/dist/channel.js
CHANGED
|
@@ -61,6 +61,9 @@ const reactions_1 = require("./reactions");
|
|
|
61
61
|
const manage_1 = require("./manage");
|
|
62
62
|
const silent_reaction_1 = require("./silent-reaction");
|
|
63
63
|
const chat_info_1 = require("./chat-info");
|
|
64
|
+
const topics_1 = require("./topics");
|
|
65
|
+
const dialogs_1 = require("./dialogs");
|
|
66
|
+
const group_tool_policy_1 = require("./group-tool-policy");
|
|
64
67
|
const secret_refs_1 = require("./secret-refs");
|
|
65
68
|
const secret_ref_runtime_1 = require("openclaw/plugin-sdk/secret-ref-runtime");
|
|
66
69
|
const group_reply_address_1 = require("./group-reply-address");
|
|
@@ -132,6 +135,10 @@ function resolveAccountReadChats(cfg, accountId) {
|
|
|
132
135
|
* `readChats`, an absent value already means "deny", so there is nothing to
|
|
133
136
|
* tell apart here.
|
|
134
137
|
*/
|
|
138
|
+
/** Chat discovery as configured; absent means "deny", like management scope. */
|
|
139
|
+
function resolveAccountDiscoverChats(cfg, accountId) {
|
|
140
|
+
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.discoverChats;
|
|
141
|
+
}
|
|
135
142
|
function resolveAccountManageChats(cfg, accountId) {
|
|
136
143
|
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.manageChats;
|
|
137
144
|
}
|
|
@@ -319,6 +326,20 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
319
326
|
aliases: ["tguserbot"],
|
|
320
327
|
},
|
|
321
328
|
capabilities: CHANNEL_CAPABILITIES,
|
|
329
|
+
// Core plans config hot reloads from these prefixes. Without the
|
|
330
|
+
// declaration a changed `channels.clawgram.*` path matches no rule and
|
|
331
|
+
// core restarts the whole Gateway (SIGUSR1, all runs aborted) — measured
|
|
332
|
+
// 2026-08-13. With it, the same edit restarts only this channel. No
|
|
333
|
+
// `noopPrefixes`: `groups`/`allowFrom`/`readChats` are read from the cfg
|
|
334
|
+
// captured in `startAccount`, so a channel restart is exactly what an
|
|
335
|
+
// edit needs to take effect.
|
|
336
|
+
reload: { configPrefixes: ["channels.clawgram"] },
|
|
337
|
+
// Per-group `tools` / `toolsBySender` from the config. Core asks the
|
|
338
|
+
// channel first because only the channel knows that its group ids carry
|
|
339
|
+
// an account prefix; see src/group-tool-policy.ts.
|
|
340
|
+
groups: {
|
|
341
|
+
resolveToolPolicy: group_tool_policy_1.resolveClawgramGroupToolPolicy,
|
|
342
|
+
},
|
|
322
343
|
agentPrompt: {
|
|
323
344
|
// Nothing here steers reactions, and that is deliberate. 2.8.0 added a
|
|
324
345
|
// `reactionGuidance` hook and 2.9.0 moved the same text onto these
|
|
@@ -337,6 +358,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
337
358
|
"For Telegram forum topics, send to the group chat id and pass the topic id separately as `threadId`.",
|
|
338
359
|
"Use the `react` action to acknowledge a message with an emoji instead of sending a reply; pass an empty `emoji` (or `remove: true`) to take the reaction back.",
|
|
339
360
|
"Use the `chatInfo` action to learn what a chat is — title, type, member count, description, pinned message — instead of guessing from its id.",
|
|
361
|
+
"Use the `topics` action to list a forum's topics by name (optional `query` narrows by title); that is where a `threadId` comes from when someone names a topic instead of quoting a message in it.",
|
|
362
|
+
"Pass that `threadId` to `read` as well: without it a forum read returns every topic interleaved rather than the one that was asked about.",
|
|
363
|
+
"Use the `dialogs` action to find out which group chats this account is actually in — including ones nobody has configured yet. It reports id, title and type only, never direct chats, and only when the account enables `discoverChats`.",
|
|
340
364
|
"Use `createGroup` (title, optional about, optional users) to create a new Telegram supergroup; `addMembers`/`removeMember` change who is in a managed chat, `promoteAdmin`/`demoteAdmin` grant or revoke admin rights, `transferOwnership` hands the chat over, `inviteLink` issues an invite link for people Telegram refused to add directly.",
|
|
341
365
|
],
|
|
342
366
|
messageToolCapabilities: () => [
|
|
@@ -345,6 +369,8 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
345
369
|
"clawgram supports Telegram forum topics via the `threadId` parameter on group sends.",
|
|
346
370
|
"clawgram can add and clear emoji reactions on messages. A plain Telegram account holds one reaction per message, so a new emoji replaces the previous one.",
|
|
347
371
|
"clawgram can describe a chat via `chatInfo`: title, type (direct/group/supergroup/channel), member count, description, whether it is a forum, and the pinned message id.",
|
|
372
|
+
"clawgram can list the topics of a forum supergroup via `topics`: id, title, last message, and whether a topic is closed, hidden or pinned.",
|
|
373
|
+
"clawgram can list the group chats the account belongs to via `dialogs`, when the account sets discoverChats. Metadata only, no direct chats — it answers \"where am I\", not \"what was said\".",
|
|
348
374
|
"clawgram can manage chats where the account's manageChats config allows it: create supergroups, add and remove members, promote and demote admins, transfer ownership, and export invite links.",
|
|
349
375
|
],
|
|
350
376
|
},
|
|
@@ -752,6 +778,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
752
778
|
ReplyToIsQuote: normalized.replyIsQuote,
|
|
753
779
|
MessageThreadId: normalized.messageThreadId,
|
|
754
780
|
NativeChannelId: normalized.chatId,
|
|
781
|
+
// Trusted per-group prompt block from `groups.<id>.systemPrompt`.
|
|
782
|
+
// Core normalizes it (`normalizeTrustedTextField`) and appends
|
|
783
|
+
// it to the system prompt for this turn. Undefined = no block.
|
|
784
|
+
GroupSystemPrompt: groupConfig.systemPrompt,
|
|
755
785
|
OriginatingChannel: "clawgram",
|
|
756
786
|
OriginatingTo: conversationRouteTarget,
|
|
757
787
|
});
|
|
@@ -867,6 +897,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
867
897
|
},
|
|
868
898
|
replyOptions: {
|
|
869
899
|
onModelSelected,
|
|
900
|
+
// `groups.<id>.skills` → core's per-turn skill allowlist.
|
|
901
|
+
// Undefined = inherit the agent's skills; [] = none here.
|
|
902
|
+
skillFilter: groupConfig.skillFilter,
|
|
870
903
|
},
|
|
871
904
|
});
|
|
872
905
|
log?.info?.("clawgram group dispatch completed", {
|
|
@@ -1293,7 +1326,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1293
1326
|
// never sees a way to send the file, announces it in words, and the
|
|
1294
1327
|
// file stays on disk. That is exactly what happened on 2026-08-07.
|
|
1295
1328
|
actions: [
|
|
1296
|
-
"send", "read", "participants", "joins", "react", "chatInfo", "upload-file",
|
|
1329
|
+
"send", "read", "participants", "joins", "react", "chatInfo", "topics", "dialogs", "upload-file",
|
|
1297
1330
|
// Chat management (2.12.0) — gated by the account's manageChats
|
|
1298
1331
|
// scope; without it every one of these is refused.
|
|
1299
1332
|
"createGroup", "addMembers", "removeMember",
|
|
@@ -1393,6 +1426,80 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1393
1426
|
participants: membership.participants,
|
|
1394
1427
|
});
|
|
1395
1428
|
}
|
|
1429
|
+
// Topic names. A forum chat is addressed by topic id, and until now an
|
|
1430
|
+
// id could only be lifted off an inbound message — so a topic nobody had
|
|
1431
|
+
// written in yet was unreachable, and one named in words was unfindable.
|
|
1432
|
+
// Titles say what a chat is working on, so the read scope gates them.
|
|
1433
|
+
if (action === "topics" || action === "forumTopics") {
|
|
1434
|
+
const topicsParams = (0, topics_1.parseTopicsParams)(params);
|
|
1435
|
+
const topicsAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1436
|
+
if (!topicsAccountId) {
|
|
1437
|
+
throw new Error("clawgram: no configured account found");
|
|
1438
|
+
}
|
|
1439
|
+
if (!(0, history_1.isChatReadable)(topicsParams.target, resolveAccountReadChats(cfg, topicsAccountId))) {
|
|
1440
|
+
actionLog.warn("clawgram topics refused: chat outside read scope", {
|
|
1441
|
+
accountId: topicsAccountId,
|
|
1442
|
+
target: topicsParams.target,
|
|
1443
|
+
});
|
|
1444
|
+
throw new Error(`clawgram: not-allowed-chat ${topicsParams.target}`);
|
|
1445
|
+
}
|
|
1446
|
+
const topicsGram = runtimes.get(topicsAccountId);
|
|
1447
|
+
if (!topicsGram) {
|
|
1448
|
+
throw new Error(`clawgram: runtime not found for account ${topicsAccountId}`);
|
|
1449
|
+
}
|
|
1450
|
+
const forum = await topicsGram.listTopics(topicsParams);
|
|
1451
|
+
actionLog.info("clawgram handleAction topics completed", {
|
|
1452
|
+
accountId: topicsAccountId,
|
|
1453
|
+
target: topicsParams.target,
|
|
1454
|
+
limit: topicsParams.limit,
|
|
1455
|
+
returned: forum.topics.length,
|
|
1456
|
+
truncated: forum.truncated,
|
|
1457
|
+
});
|
|
1458
|
+
return (0, core_1.jsonResult)({
|
|
1459
|
+
ok: true,
|
|
1460
|
+
accountId: topicsAccountId,
|
|
1461
|
+
chatId: forum.chatId ?? topicsParams.target,
|
|
1462
|
+
count: forum.topics.length,
|
|
1463
|
+
truncated: forum.truncated,
|
|
1464
|
+
topics: forum.topics,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
// Which chats this account is in. Not gated by `readChats` — the whole
|
|
1468
|
+
// point is to find chats that are not in it yet — so it has a gate of
|
|
1469
|
+
// its own, is metadata only, and never reports direct chats.
|
|
1470
|
+
if (action === "dialogs" || action === "chats") {
|
|
1471
|
+
const dialogsParams = (0, dialogs_1.parseDialogsParams)(params);
|
|
1472
|
+
const dialogsAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1473
|
+
if (!dialogsAccountId) {
|
|
1474
|
+
throw new Error("clawgram: no configured account found");
|
|
1475
|
+
}
|
|
1476
|
+
if (!(0, dialogs_1.isChatDiscoveryEnabled)(resolveAccountDiscoverChats(cfg, dialogsAccountId))) {
|
|
1477
|
+
actionLog.warn("clawgram dialogs refused: chat-discovery is not enabled", {
|
|
1478
|
+
accountId: dialogsAccountId,
|
|
1479
|
+
});
|
|
1480
|
+
throw new Error("clawgram: chat-discovery is not enabled");
|
|
1481
|
+
}
|
|
1482
|
+
const dialogsGram = runtimes.get(dialogsAccountId);
|
|
1483
|
+
if (!dialogsGram) {
|
|
1484
|
+
throw new Error(`clawgram: runtime not found for account ${dialogsAccountId}`);
|
|
1485
|
+
}
|
|
1486
|
+
const found = await dialogsGram.listDialogs(dialogsParams);
|
|
1487
|
+
// Counts only: which chats a person's account sits in is exactly the
|
|
1488
|
+
// kind of thing that should not be sitting in a log.
|
|
1489
|
+
actionLog.info("clawgram handleAction dialogs completed", {
|
|
1490
|
+
accountId: dialogsAccountId,
|
|
1491
|
+
limit: dialogsParams.limit,
|
|
1492
|
+
returned: found.dialogs.length,
|
|
1493
|
+
truncated: found.truncated,
|
|
1494
|
+
});
|
|
1495
|
+
return (0, core_1.jsonResult)({
|
|
1496
|
+
ok: true,
|
|
1497
|
+
accountId: dialogsAccountId,
|
|
1498
|
+
count: found.dialogs.length,
|
|
1499
|
+
truncated: found.truncated,
|
|
1500
|
+
dialogs: found.dialogs,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1396
1503
|
// Where this account was recently added, and by whom. Reading the journal
|
|
1397
1504
|
// has no scope check of its own: it only ever contains chats this account
|
|
1398
1505
|
// was put into, which is exactly what the caller is allowed to learn.
|
package/dist/chat-info.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.parseChatInfoParams = parseChatInfoParams;
|
|
19
19
|
exports.describeChat = describeChat;
|
|
20
|
+
const helpers_1 = require("./helpers");
|
|
20
21
|
function parseChatInfoParams(params, toolContext) {
|
|
21
22
|
const rawTarget = params.chatId ?? params.target ?? params.to ?? params.chat ?? toolContext?.currentChannelId;
|
|
22
23
|
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
@@ -78,7 +79,9 @@ function describeChat(entity, full) {
|
|
|
78
79
|
chatId: readId(raw?.id),
|
|
79
80
|
type,
|
|
80
81
|
title: type === "direct" ? resolveUserTitle(raw) : readString(raw?.title),
|
|
81
|
-
|
|
82
|
+
// Not `raw.username`: an account or chat holding more than one handle keeps
|
|
83
|
+
// them in `usernames[]` and leaves the legacy field empty.
|
|
84
|
+
username: (0, helpers_1.resolveActiveUsername)(raw),
|
|
82
85
|
about: readString(fullChat?.about),
|
|
83
86
|
pinnedMessageId: readId(fullChat?.pinnedMsgId),
|
|
84
87
|
};
|
package/dist/dialogs.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DIALOGS_MAX_LIMIT = exports.DIALOGS_DEFAULT_LIMIT = void 0;
|
|
4
|
+
exports.parseDialogsParams = parseDialogsParams;
|
|
5
|
+
exports.isChatDiscoveryEnabled = isChatDiscoveryEnabled;
|
|
6
|
+
exports.normalizeDialogs = normalizeDialogs;
|
|
7
|
+
/**
|
|
8
|
+
* Which chats this account is actually in.
|
|
9
|
+
*
|
|
10
|
+
* Everything else here answers questions about a chat the caller can already
|
|
11
|
+
* name. Nothing answered "where am I now" — the assistant learned that from a
|
|
12
|
+
* service message Telegram sends when somebody adds it, and large supergroups
|
|
13
|
+
* do not send one. A chat could therefore hold the account for weeks while
|
|
14
|
+
* every message from it was dropped as "group not present in groups config",
|
|
15
|
+
* with no trace anywhere that the account was even a member.
|
|
16
|
+
*
|
|
17
|
+
* Discovery is metadata only — id, title, type — and never direct chats: a
|
|
18
|
+
* personal account also sits in family and one-to-one conversations, and
|
|
19
|
+
* enumerating those is the surveillance the read scope exists to prevent.
|
|
20
|
+
* It stays off until the account sets `discoverChats`.
|
|
21
|
+
*/
|
|
22
|
+
const normalize_js_1 = require("./normalize.js");
|
|
23
|
+
exports.DIALOGS_DEFAULT_LIMIT = 100;
|
|
24
|
+
exports.DIALOGS_MAX_LIMIT = 500;
|
|
25
|
+
function parseDialogsParams(params) {
|
|
26
|
+
const rawQuery = params.query ?? params.search ?? params.title;
|
|
27
|
+
const trimmedQuery = typeof rawQuery === "string" ? rawQuery.trim() : "";
|
|
28
|
+
const query = trimmedQuery.length > 0 ? trimmedQuery : undefined;
|
|
29
|
+
const rawLimit = params.limit;
|
|
30
|
+
if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
|
|
31
|
+
return { limit: exports.DIALOGS_DEFAULT_LIMIT, query };
|
|
32
|
+
}
|
|
33
|
+
const parsed = Number(rawLimit);
|
|
34
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
35
|
+
throw new Error("clawgram: dialogs limit must be a positive number");
|
|
36
|
+
}
|
|
37
|
+
return { limit: Math.min(Math.floor(parsed), exports.DIALOGS_MAX_LIMIT), query };
|
|
38
|
+
}
|
|
39
|
+
/** Chat discovery is off unless the account asks for it. */
|
|
40
|
+
function isChatDiscoveryEnabled(discoverChats) {
|
|
41
|
+
return discoverChats === true;
|
|
42
|
+
}
|
|
43
|
+
function resolveDialogType(dialog) {
|
|
44
|
+
if (dialog.isUser === true)
|
|
45
|
+
return undefined;
|
|
46
|
+
const entity = (dialog.entity ?? {});
|
|
47
|
+
if (dialog.isChannel === true) {
|
|
48
|
+
// Telegram models supergroups and broadcast channels with one constructor;
|
|
49
|
+
// `megagroup` is what separates "a group with history" from "a feed".
|
|
50
|
+
return entity.megagroup === true ? "supergroup" : "channel";
|
|
51
|
+
}
|
|
52
|
+
return dialog.isGroup === true ? "group" : undefined;
|
|
53
|
+
}
|
|
54
|
+
function matchesQuery(title, query) {
|
|
55
|
+
if (!query)
|
|
56
|
+
return true;
|
|
57
|
+
if (!title)
|
|
58
|
+
return false;
|
|
59
|
+
return title.toLocaleLowerCase().includes(query.toLocaleLowerCase());
|
|
60
|
+
}
|
|
61
|
+
function normalizeDialogs(raw, options = {}) {
|
|
62
|
+
if (!Array.isArray(raw))
|
|
63
|
+
return [];
|
|
64
|
+
const dialogs = [];
|
|
65
|
+
for (const entry of raw) {
|
|
66
|
+
if (!entry || typeof entry !== "object")
|
|
67
|
+
continue;
|
|
68
|
+
const dialog = entry;
|
|
69
|
+
const type = resolveDialogType(dialog);
|
|
70
|
+
if (!type)
|
|
71
|
+
continue;
|
|
72
|
+
const chatId = (0, normalize_js_1.toStringId)(dialog.id);
|
|
73
|
+
if (!chatId)
|
|
74
|
+
continue;
|
|
75
|
+
const entity = (dialog.entity ?? {});
|
|
76
|
+
const rawTitle = dialog.title ?? entity.title;
|
|
77
|
+
const title = typeof rawTitle === "string" && rawTitle.length > 0 ? rawTitle : undefined;
|
|
78
|
+
if (!matchesQuery(title, options.query))
|
|
79
|
+
continue;
|
|
80
|
+
const summary = { chatId, type };
|
|
81
|
+
if (title !== undefined)
|
|
82
|
+
summary.title = title;
|
|
83
|
+
if (entity.forum === true)
|
|
84
|
+
summary.isForum = true;
|
|
85
|
+
dialogs.push(summary);
|
|
86
|
+
}
|
|
87
|
+
return dialogs;
|
|
88
|
+
}
|
package/dist/gramjs-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GramJsClientManager = void 0;
|
|
4
|
+
exports.buildParticipantsQuery = buildParticipantsQuery;
|
|
4
5
|
exports.buildVoiceNoteParams = buildVoiceNoteParams;
|
|
5
6
|
const telegram_1 = require("telegram");
|
|
6
7
|
const sessions_1 = require("telegram/sessions");
|
|
@@ -12,6 +13,8 @@ const html_render_1 = require("./html-render");
|
|
|
12
13
|
const proxy_config_1 = require("./proxy-config");
|
|
13
14
|
const secret_refs_1 = require("./secret-refs");
|
|
14
15
|
const history_1 = require("./history");
|
|
16
|
+
const topics_1 = require("./topics");
|
|
17
|
+
const dialogs_1 = require("./dialogs");
|
|
15
18
|
const manage_1 = require("./manage");
|
|
16
19
|
function toStringId(value) {
|
|
17
20
|
if (value === null || value === undefined)
|
|
@@ -85,6 +88,22 @@ function parseTargetWithThread(rawTarget) {
|
|
|
85
88
|
chatId: raw,
|
|
86
89
|
};
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Membership query for `getParticipants`.
|
|
93
|
+
*
|
|
94
|
+
* The filter has to arrive as a TL constructor. `getParticipants` accepts an
|
|
95
|
+
* unknown `filter` value without complaining and falls back to everyone, so a
|
|
96
|
+
* plain `"admins"` string would return the whole chat under a name promising
|
|
97
|
+
* otherwise — and an allowFrom rebuilt from that list would open a 1000-person
|
|
98
|
+
* chat to all of it.
|
|
99
|
+
*/
|
|
100
|
+
function buildParticipantsQuery(args) {
|
|
101
|
+
const query = { limit: args.limit };
|
|
102
|
+
if (args.filter === "admins") {
|
|
103
|
+
query.filter = new telegram_1.Api.ChannelParticipantsAdmins();
|
|
104
|
+
}
|
|
105
|
+
return query;
|
|
106
|
+
}
|
|
88
107
|
/**
|
|
89
108
|
* Voice-message option for `sendFile`.
|
|
90
109
|
*
|
|
@@ -442,7 +461,13 @@ class GramJsClientManager {
|
|
|
442
461
|
*/
|
|
443
462
|
async listMessages(args) {
|
|
444
463
|
const resolved = await this.resolvePeer(args.target);
|
|
445
|
-
|
|
464
|
+
// A topic can be named two ways — `chatId:topic:N` in the target or a
|
|
465
|
+
// `threadId` parameter beside it. The explicit parameter wins; both used to
|
|
466
|
+
// be parsed and then dropped before the query was built.
|
|
467
|
+
const query = (0, history_1.buildHistoryQuery)({
|
|
468
|
+
...args,
|
|
469
|
+
messageThreadId: args.messageThreadId ?? resolved.messageThreadId,
|
|
470
|
+
});
|
|
446
471
|
const fetched = await this.client.getMessages(resolved.peer, query);
|
|
447
472
|
const raw = Array.isArray(fetched) ? fetched : [];
|
|
448
473
|
return {
|
|
@@ -462,37 +487,50 @@ class GramJsClientManager {
|
|
|
462
487
|
*/
|
|
463
488
|
async listParticipants(args) {
|
|
464
489
|
const resolved = await this.resolvePeer(args.target);
|
|
465
|
-
const fetched = await this.client.getParticipants(resolved.peer,
|
|
466
|
-
limit: args.limit,
|
|
467
|
-
});
|
|
490
|
+
const fetched = await this.client.getParticipants(resolved.peer, buildParticipantsQuery(args));
|
|
468
491
|
const raw = Array.isArray(fetched) ? fetched : [];
|
|
469
|
-
const participants = [];
|
|
470
|
-
for (const entry of raw) {
|
|
471
|
-
const rawId = entry?.id;
|
|
472
|
-
if (rawId === undefined || rawId === null) {
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
const username = typeof entry?.username === "string" && entry.username.length > 0
|
|
476
|
-
? entry.username
|
|
477
|
-
: undefined;
|
|
478
|
-
const member = {
|
|
479
|
-
userId: String(rawId),
|
|
480
|
-
username,
|
|
481
|
-
isBot: entry?.bot === true,
|
|
482
|
-
};
|
|
483
|
-
// Display names are personal data, so they are opt-in: only the identity
|
|
484
|
-
// linking flow asks for them, and it discards them once a link is made.
|
|
485
|
-
if (args.includeNames) {
|
|
486
|
-
if (typeof entry?.firstName === "string" && entry.firstName.length > 0)
|
|
487
|
-
member.firstName = entry.firstName;
|
|
488
|
-
if (typeof entry?.lastName === "string" && entry.lastName.length > 0)
|
|
489
|
-
member.lastName = entry.lastName;
|
|
490
|
-
}
|
|
491
|
-
participants.push(member);
|
|
492
|
-
}
|
|
493
492
|
return {
|
|
494
493
|
chatId: resolved.chatId,
|
|
495
|
-
participants,
|
|
494
|
+
participants: (0, history_1.normalizeParticipants)(raw, { includeNames: args.includeNames }),
|
|
495
|
+
truncated: raw.length >= args.limit,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The group chats this account belongs to.
|
|
500
|
+
*
|
|
501
|
+
* Deliberately thin: `getDialogs` also returns every private conversation,
|
|
502
|
+
* and `normalizeDialogs` drops them before anything else sees the list.
|
|
503
|
+
*/
|
|
504
|
+
async listDialogs(args) {
|
|
505
|
+
const fetched = await this.client.getDialogs({ limit: args.limit });
|
|
506
|
+
const raw = Array.isArray(fetched) ? fetched : [];
|
|
507
|
+
return {
|
|
508
|
+
dialogs: (0, dialogs_1.normalizeDialogs)(raw, { query: args.query }),
|
|
509
|
+
truncated: raw.length >= args.limit,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Topics of a forum supergroup, by name.
|
|
514
|
+
*
|
|
515
|
+
* `q` is passed to Telegram when the caller narrows the list, and the same
|
|
516
|
+
* text is applied again to the result: the server-side search is not
|
|
517
|
+
* guaranteed to be there on every layer, and a filter that silently does
|
|
518
|
+
* nothing is worse than one that runs twice.
|
|
519
|
+
*/
|
|
520
|
+
async listTopics(args) {
|
|
521
|
+
const resolved = await this.resolvePeer(args.target, { kind: "channel" });
|
|
522
|
+
const result = await this.client.invoke(new telegram_1.Api.channels.GetForumTopics({
|
|
523
|
+
channel: resolved.peer,
|
|
524
|
+
...(args.query ? { q: args.query } : {}),
|
|
525
|
+
offsetDate: 0,
|
|
526
|
+
offsetId: 0,
|
|
527
|
+
offsetTopic: 0,
|
|
528
|
+
limit: args.limit,
|
|
529
|
+
}));
|
|
530
|
+
const raw = Array.isArray(result?.topics) ? result.topics : [];
|
|
531
|
+
return {
|
|
532
|
+
chatId: resolved.chatId,
|
|
533
|
+
topics: (0, topics_1.normalizeForumTopics)(raw, { query: args.query }),
|
|
496
534
|
truncated: raw.length >= args.limit,
|
|
497
535
|
};
|
|
498
536
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveClawgramGroupToolPolicy = resolveClawgramGroupToolPolicy;
|
|
4
|
+
const channel_policy_1 = require("openclaw/plugin-sdk/channel-policy");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
const helpers_1 = require("./helpers");
|
|
7
|
+
/**
|
|
8
|
+
* `groups.resolveToolPolicy` — core calls this before its own lookup when a
|
|
9
|
+
* message from a group session runs. The only thing this channel adds is the
|
|
10
|
+
* id translation: core passes the scoped peer id (`<accountId>:<chatId>`),
|
|
11
|
+
* the config is keyed by the bare chat id. Everything else — per-account vs
|
|
12
|
+
* top-level `groups`, the `*` default, `toolsBySender` by id/username/name —
|
|
13
|
+
* is the SDK's `resolveChannelGroupToolsPolicy`, the same function the
|
|
14
|
+
* bundled Telegram channel delegates to.
|
|
15
|
+
*
|
|
16
|
+
* What the returned policy governs: gateway tools (message, sessions_*,
|
|
17
|
+
* cron, memory_*, …) — under CLI backends via the loopback MCP tool list.
|
|
18
|
+
* Not the CLI backend's own exec/read/write; those are per agent, not per
|
|
19
|
+
* group, and only a separate agent bound to the chat restricts them.
|
|
20
|
+
*/
|
|
21
|
+
function resolveClawgramGroupToolPolicy(ctx) {
|
|
22
|
+
const groupId = (0, helpers_1.stripAccountScopedGroupId)(ctx.groupId, ctx.accountId);
|
|
23
|
+
if (!groupId) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return (0, channel_policy_1.resolveChannelGroupToolsPolicy)({
|
|
27
|
+
cfg: ctx.cfg,
|
|
28
|
+
channel: constants_1.CHANNEL_ID,
|
|
29
|
+
groupId,
|
|
30
|
+
accountId: ctx.accountId ?? "default",
|
|
31
|
+
senderId: ctx.senderId,
|
|
32
|
+
senderName: ctx.senderName,
|
|
33
|
+
senderUsername: ctx.senderUsername,
|
|
34
|
+
senderE164: ctx.senderE164,
|
|
35
|
+
});
|
|
36
|
+
}
|
package/dist/helpers.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.inferOutboundTargetKind = inferOutboundTargetKind;
|
|
|
10
10
|
exports.routeKindFromChatType = routeKindFromChatType;
|
|
11
11
|
exports.buildConversationTarget = buildConversationTarget;
|
|
12
12
|
exports.buildScopedGroupPeerId = buildScopedGroupPeerId;
|
|
13
|
+
exports.stripAccountScopedGroupId = stripAccountScopedGroupId;
|
|
13
14
|
exports.stripReplyDirectiveTags = stripReplyDirectiveTags;
|
|
14
15
|
exports.readLatestAssistantFallbackFromTranscript = readLatestAssistantFallbackFromTranscript;
|
|
15
16
|
exports.resolveActionTarget = resolveActionTarget;
|
|
@@ -21,6 +22,7 @@ exports.resolveAllowFrom = resolveAllowFrom;
|
|
|
21
22
|
exports.resolveGroupPolicy = resolveGroupPolicy;
|
|
22
23
|
exports.resolveGroups = resolveGroups;
|
|
23
24
|
exports.resolveGroupConfig = resolveGroupConfig;
|
|
25
|
+
exports.resolveGroupPromptSettings = resolveGroupPromptSettings;
|
|
24
26
|
exports.resolveActiveUsername = resolveActiveUsername;
|
|
25
27
|
exports.normalizeAllowEntry = normalizeAllowEntry;
|
|
26
28
|
exports.isSenderAllowed = isSenderAllowed;
|
|
@@ -89,6 +91,20 @@ function buildScopedGroupPeerId(accountId, chatId) {
|
|
|
89
91
|
const scopedAccountId = (accountId ?? "default").trim() || "default";
|
|
90
92
|
return `${scopedAccountId}:${chatId}`;
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Inverse of `buildScopedGroupPeerId`. Core derives group ids from the session
|
|
96
|
+
* key, so a channel hook receives `<accountId>:<chatId>` while `groups` in the
|
|
97
|
+
* config is keyed by the bare chat id. Only this account's prefix is stripped;
|
|
98
|
+
* anything else passes through untouched.
|
|
99
|
+
*/
|
|
100
|
+
function stripAccountScopedGroupId(groupId, accountId) {
|
|
101
|
+
const raw = typeof groupId === "string" ? groupId.trim() : "";
|
|
102
|
+
if (!raw) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const prefix = `${(accountId ?? "default").trim() || "default"}:`;
|
|
106
|
+
return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
107
|
+
}
|
|
92
108
|
function stripReplyDirectiveTags(text) {
|
|
93
109
|
return text
|
|
94
110
|
.replace(/\[\[\s*reply_to_current\s*\]\]/gi, " ")
|
|
@@ -258,6 +274,28 @@ function resolveAllowFrom(value) {
|
|
|
258
274
|
function resolveGroupPolicy(value) {
|
|
259
275
|
return value === "open" ? "open" : "mention";
|
|
260
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Per-group `skills` → core `replyOptions.skillFilter`, `systemPrompt` →
|
|
279
|
+
* `GroupSystemPrompt`. An empty `skills` array is kept as `[]` — "no skills
|
|
280
|
+
* in this chat" is an answer, the same one core gives `agents.list[].skills: []`
|
|
281
|
+
* — while a blank `systemPrompt` is unset rather than an empty trusted block.
|
|
282
|
+
*/
|
|
283
|
+
function resolveGroupPromptSettings(groupConfig) {
|
|
284
|
+
const settings = {};
|
|
285
|
+
if (!groupConfig) {
|
|
286
|
+
return settings;
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(groupConfig.skills)) {
|
|
289
|
+
settings.skillFilter = groupConfig.skills
|
|
290
|
+
.filter((entry) => typeof entry === "string")
|
|
291
|
+
.map((entry) => entry.trim())
|
|
292
|
+
.filter(Boolean);
|
|
293
|
+
}
|
|
294
|
+
if (typeof groupConfig.systemPrompt === "string" && groupConfig.systemPrompt.trim()) {
|
|
295
|
+
settings.systemPrompt = groupConfig.systemPrompt.trim();
|
|
296
|
+
}
|
|
297
|
+
return settings;
|
|
298
|
+
}
|
|
261
299
|
function resolveGroups(value) {
|
|
262
300
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
263
301
|
return {};
|
|
@@ -273,6 +311,7 @@ function resolveGroups(value) {
|
|
|
273
311
|
enabled: groupConfig.enabled !== false,
|
|
274
312
|
groupPolicy: resolveGroupPolicy(groupConfig.groupPolicy),
|
|
275
313
|
allowFrom: resolveAllowFrom(groupConfig.allowFrom),
|
|
314
|
+
...resolveGroupPromptSettings(groupConfig),
|
|
276
315
|
},
|
|
277
316
|
];
|
|
278
317
|
}).filter(([groupId]) => Boolean(groupId)));
|
package/dist/history.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.parseTimeBoundary = parseTimeBoundary;
|
|
|
16
16
|
exports.parseLimit = parseLimit;
|
|
17
17
|
exports.parseMessageId = parseMessageId;
|
|
18
18
|
exports.parseListMessagesParams = parseListMessagesParams;
|
|
19
|
+
exports.normalizeParticipants = normalizeParticipants;
|
|
19
20
|
exports.parseListParticipantsParams = parseListParticipantsParams;
|
|
20
21
|
exports.buildHistoryQuery = buildHistoryQuery;
|
|
21
22
|
exports.normalizeChatKey = normalizeChatKey;
|
|
@@ -26,6 +27,7 @@ exports.collectHistoryWindow = collectHistoryWindow;
|
|
|
26
27
|
exports.HISTORY_DEFAULT_LIMIT = 100;
|
|
27
28
|
exports.HISTORY_MAX_LIMIT = 500;
|
|
28
29
|
const media_1 = require("./media");
|
|
30
|
+
const helpers_1 = require("./helpers");
|
|
29
31
|
/**
|
|
30
32
|
* Matches `normalize.ts` and `gramjs-client.ts` deliberately.
|
|
31
33
|
*
|
|
@@ -119,6 +121,7 @@ function parseListMessagesParams(params) {
|
|
|
119
121
|
if (since !== undefined && until !== undefined && since > until) {
|
|
120
122
|
throw new Error("clawgram: since must not be later than until");
|
|
121
123
|
}
|
|
124
|
+
const rawThreadId = params.threadId ?? params.topicId ?? params.messageThreadId ?? params.topic;
|
|
122
125
|
return {
|
|
123
126
|
target,
|
|
124
127
|
limit: parseLimit(params.limit),
|
|
@@ -126,10 +129,61 @@ function parseListMessagesParams(params) {
|
|
|
126
129
|
until,
|
|
127
130
|
minId: parseMessageId(params.after, "after"),
|
|
128
131
|
maxId: parseMessageId(params.before, "before"),
|
|
132
|
+
messageThreadId: parseMessageId(rawThreadId, "threadId"),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
exports.PARTICIPANTS_DEFAULT_LIMIT = 200;
|
|
132
136
|
exports.PARTICIPANTS_MAX_LIMIT = 1000;
|
|
137
|
+
/**
|
|
138
|
+
* Membership as the caller sees it.
|
|
139
|
+
*
|
|
140
|
+
* The handle comes through `resolveActiveUsername`, not off the raw field:
|
|
141
|
+
* once an account holds more than one username — several handles, or a
|
|
142
|
+
* collectible one — Telegram moves them into `usernames[]` and leaves the
|
|
143
|
+
* legacy `username` EMPTY. Reading the raw field is why the owner of this
|
|
144
|
+
* deployment appeared in every generated table as "(без тэга)" beside a bare
|
|
145
|
+
* numeric id, the only person without a handle in chats of 23, 9, 7 and 3.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeParticipants(raw, options) {
|
|
148
|
+
if (!Array.isArray(raw))
|
|
149
|
+
return [];
|
|
150
|
+
const participants = [];
|
|
151
|
+
for (const entry of raw) {
|
|
152
|
+
if (!entry || typeof entry !== "object")
|
|
153
|
+
continue;
|
|
154
|
+
const candidate = entry;
|
|
155
|
+
const userId = toStringId(candidate.id);
|
|
156
|
+
if (!userId)
|
|
157
|
+
continue;
|
|
158
|
+
const member = {
|
|
159
|
+
userId,
|
|
160
|
+
username: (0, helpers_1.resolveActiveUsername)(candidate),
|
|
161
|
+
isBot: candidate.bot === true,
|
|
162
|
+
};
|
|
163
|
+
// Display names are personal data, so they are opt-in: only the identity
|
|
164
|
+
// linking flow asks for them, and it discards them once a link is made.
|
|
165
|
+
if (options.includeNames) {
|
|
166
|
+
if (typeof candidate.firstName === "string" && candidate.firstName.length > 0) {
|
|
167
|
+
member.firstName = candidate.firstName;
|
|
168
|
+
}
|
|
169
|
+
if (typeof candidate.lastName === "string" && candidate.lastName.length > 0) {
|
|
170
|
+
member.lastName = candidate.lastName;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
participants.push(member);
|
|
174
|
+
}
|
|
175
|
+
return participants;
|
|
176
|
+
}
|
|
177
|
+
function parseParticipantsFilter(params) {
|
|
178
|
+
if (params.admins === true)
|
|
179
|
+
return "admins";
|
|
180
|
+
const raw = params.filter;
|
|
181
|
+
if (raw === undefined || raw === null || raw === "")
|
|
182
|
+
return "all";
|
|
183
|
+
if (raw === "all" || raw === "admins")
|
|
184
|
+
return raw;
|
|
185
|
+
throw new Error('clawgram: participants filter must be "all" or "admins"');
|
|
186
|
+
}
|
|
133
187
|
/**
|
|
134
188
|
* Membership is asked for by chat, so a target is required. `limit` is clamped
|
|
135
189
|
* for the same reason it is clamped when reading history: a large group must
|
|
@@ -142,15 +196,16 @@ function parseListParticipantsParams(params) {
|
|
|
142
196
|
throw new Error("clawgram: participants requires a chatId");
|
|
143
197
|
}
|
|
144
198
|
const includeNames = params.includeNames === true || params.includeNames === "true";
|
|
199
|
+
const filter = parseParticipantsFilter(params);
|
|
145
200
|
const rawLimit = params.limit;
|
|
146
201
|
if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
|
|
147
|
-
return { target, limit: exports.PARTICIPANTS_DEFAULT_LIMIT, includeNames };
|
|
202
|
+
return { target, limit: exports.PARTICIPANTS_DEFAULT_LIMIT, includeNames, filter };
|
|
148
203
|
}
|
|
149
204
|
const parsed = Number(rawLimit);
|
|
150
205
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
151
206
|
throw new Error("clawgram: participants limit must be a positive number");
|
|
152
207
|
}
|
|
153
|
-
return { target, limit: Math.min(Math.floor(parsed), exports.PARTICIPANTS_MAX_LIMIT), includeNames };
|
|
208
|
+
return { target, limit: Math.min(Math.floor(parsed), exports.PARTICIPANTS_MAX_LIMIT), includeNames, filter };
|
|
154
209
|
}
|
|
155
210
|
/**
|
|
156
211
|
* Builds the GramJS query for a window.
|
|
@@ -164,6 +219,11 @@ function parseListParticipantsParams(params) {
|
|
|
164
219
|
*/
|
|
165
220
|
function buildHistoryQuery(args) {
|
|
166
221
|
const query = { limit: args.limit };
|
|
222
|
+
// GramJS turns `replyTo` into messages.GetReplies, which is Telegram's way of
|
|
223
|
+
// asking for one forum topic rather than the whole chat.
|
|
224
|
+
if (args.messageThreadId !== undefined) {
|
|
225
|
+
query.replyTo = args.messageThreadId;
|
|
226
|
+
}
|
|
167
227
|
if (args.until !== undefined) {
|
|
168
228
|
query.offsetDate = args.until + 1;
|
|
169
229
|
}
|
package/dist/normalize.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.toStringId = toStringId;
|
|
|
4
4
|
exports.toPeerChatId = toPeerChatId;
|
|
5
5
|
exports.toPeerChannelId = toPeerChannelId;
|
|
6
6
|
exports.normalizeTelegramEvent = normalizeTelegramEvent;
|
|
7
|
+
const helpers_1 = require("./helpers");
|
|
7
8
|
function toStringId(value) {
|
|
8
9
|
if (value === null || value === undefined)
|
|
9
10
|
return undefined;
|
|
@@ -106,11 +107,10 @@ function normalizeTelegramEvent(event, accountId) {
|
|
|
106
107
|
? msg.text
|
|
107
108
|
: undefined;
|
|
108
109
|
const chatType = inferTelegramChatType(msg, chatId);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: undefined;
|
|
110
|
+
// Not the raw field: a sender holding several handles (or a collectible one)
|
|
111
|
+
// keeps them in `usernames[]` and leaves `username` empty, so an allowFrom
|
|
112
|
+
// entry written as `@handle` would silently never match that person.
|
|
113
|
+
const senderUsername = (0, helpers_1.resolveActiveUsername)(msg.sender) ?? (0, helpers_1.resolveActiveUsername)(msg._sender);
|
|
114
114
|
const senderDisplay = typeof msg.sender?.firstName === "string"
|
|
115
115
|
? [msg.sender.firstName, msg.sender.lastName].filter(Boolean).join(" ").trim()
|
|
116
116
|
: typeof msg._sender?.firstName === "string"
|
package/dist/topics.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TOPICS_MAX_LIMIT = exports.TOPICS_DEFAULT_LIMIT = void 0;
|
|
4
|
+
exports.parseTopicsParams = parseTopicsParams;
|
|
5
|
+
exports.normalizeForumTopics = normalizeForumTopics;
|
|
6
|
+
/**
|
|
7
|
+
* Forum topics — the names a supergroup organises itself by.
|
|
8
|
+
*
|
|
9
|
+
* A forum chat addresses replies by topic, and `chatInfo` can only say *that*
|
|
10
|
+
* a chat is a forum. Everything else about a topic reached the assistant as a
|
|
11
|
+
* bare number lifted off an inbound message, so a person could ask it to work
|
|
12
|
+
* in "Визитка - представление" and it had no way to turn that name into an id —
|
|
13
|
+
* or to know the topic existed before somebody wrote in it.
|
|
14
|
+
*
|
|
15
|
+
* Parsing and shaping are pure so they can be tested without a Telegram
|
|
16
|
+
* client; the transport lives in `GramJsClientManager.listTopics`.
|
|
17
|
+
*/
|
|
18
|
+
const normalize_js_1 = require("./normalize.js");
|
|
19
|
+
exports.TOPICS_DEFAULT_LIMIT = 100;
|
|
20
|
+
exports.TOPICS_MAX_LIMIT = 500;
|
|
21
|
+
function parseTopicsParams(params) {
|
|
22
|
+
const rawTarget = params.chatId ?? params.target ?? params.to ?? params.chat;
|
|
23
|
+
const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
|
|
24
|
+
if (!target) {
|
|
25
|
+
throw new Error("clawgram: topics requires a chatId");
|
|
26
|
+
}
|
|
27
|
+
const rawQuery = params.query ?? params.search ?? params.title;
|
|
28
|
+
const trimmedQuery = typeof rawQuery === "string" ? rawQuery.trim() : "";
|
|
29
|
+
const query = trimmedQuery.length > 0 ? trimmedQuery : undefined;
|
|
30
|
+
const rawLimit = params.limit;
|
|
31
|
+
if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
|
|
32
|
+
return { target, limit: exports.TOPICS_DEFAULT_LIMIT, query };
|
|
33
|
+
}
|
|
34
|
+
const parsed = Number(rawLimit);
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
36
|
+
throw new Error("clawgram: topics limit must be a positive number");
|
|
37
|
+
}
|
|
38
|
+
return { target, limit: Math.min(Math.floor(parsed), exports.TOPICS_MAX_LIMIT), query };
|
|
39
|
+
}
|
|
40
|
+
/** Telegram sends flags only when they are set; an absent flag is not `false`. */
|
|
41
|
+
function optionalFlag(value) {
|
|
42
|
+
return typeof value === "boolean" ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function matchesQuery(title, query) {
|
|
45
|
+
if (!query)
|
|
46
|
+
return true;
|
|
47
|
+
return title.toLocaleLowerCase().includes(query.toLocaleLowerCase());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `channels.getForumTopics` returns live topics and tombstones in one list.
|
|
51
|
+
* A `ForumTopicDeleted` carries an id and nothing else: it cannot be named,
|
|
52
|
+
* so it is dropped rather than surfaced as a topic with a blank title.
|
|
53
|
+
*/
|
|
54
|
+
function normalizeForumTopics(raw, options = {}) {
|
|
55
|
+
if (!Array.isArray(raw))
|
|
56
|
+
return [];
|
|
57
|
+
const topics = [];
|
|
58
|
+
for (const entry of raw) {
|
|
59
|
+
if (!entry || typeof entry !== "object")
|
|
60
|
+
continue;
|
|
61
|
+
const candidate = entry;
|
|
62
|
+
const title = typeof candidate.title === "string" ? candidate.title : undefined;
|
|
63
|
+
const topicId = (0, normalize_js_1.toStringId)(candidate.id);
|
|
64
|
+
if (!title || !topicId)
|
|
65
|
+
continue;
|
|
66
|
+
if (!matchesQuery(title, options.query))
|
|
67
|
+
continue;
|
|
68
|
+
const topic = { topicId, title };
|
|
69
|
+
const topMessageId = (0, normalize_js_1.toStringId)(candidate.topMessage);
|
|
70
|
+
if (topMessageId !== undefined)
|
|
71
|
+
topic.topMessageId = topMessageId;
|
|
72
|
+
const closed = optionalFlag(candidate.closed);
|
|
73
|
+
if (closed !== undefined)
|
|
74
|
+
topic.closed = closed;
|
|
75
|
+
const hidden = optionalFlag(candidate.hidden);
|
|
76
|
+
if (hidden !== undefined)
|
|
77
|
+
topic.hidden = hidden;
|
|
78
|
+
const pinned = optionalFlag(candidate.pinned);
|
|
79
|
+
if (pinned !== undefined)
|
|
80
|
+
topic.pinned = pinned;
|
|
81
|
+
topics.push(topic);
|
|
82
|
+
}
|
|
83
|
+
return topics;
|
|
84
|
+
}
|
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.17.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -69,6 +69,69 @@
|
|
|
69
69
|
}
|
|
70
70
|
]
|
|
71
71
|
}
|
|
72
|
+
},
|
|
73
|
+
"tools": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"allow": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"alsoAllow": {
|
|
84
|
+
"type": "array",
|
|
85
|
+
"items": {
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"deny": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"items": {
|
|
92
|
+
"type": "string"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"toolsBySender": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"propertyNames": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"additionalProperties": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"additionalProperties": false,
|
|
105
|
+
"properties": {
|
|
106
|
+
"allow": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"alsoAllow": {
|
|
113
|
+
"type": "array",
|
|
114
|
+
"items": {
|
|
115
|
+
"type": "string"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"deny": {
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "string"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"skills": {
|
|
128
|
+
"type": "array",
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"systemPrompt": {
|
|
134
|
+
"type": "string"
|
|
72
135
|
}
|
|
73
136
|
}
|
|
74
137
|
}
|
|
@@ -311,6 +374,69 @@
|
|
|
311
374
|
}
|
|
312
375
|
]
|
|
313
376
|
}
|
|
377
|
+
},
|
|
378
|
+
"tools": {
|
|
379
|
+
"type": "object",
|
|
380
|
+
"additionalProperties": false,
|
|
381
|
+
"properties": {
|
|
382
|
+
"allow": {
|
|
383
|
+
"type": "array",
|
|
384
|
+
"items": {
|
|
385
|
+
"type": "string"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
"alsoAllow": {
|
|
389
|
+
"type": "array",
|
|
390
|
+
"items": {
|
|
391
|
+
"type": "string"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"deny": {
|
|
395
|
+
"type": "array",
|
|
396
|
+
"items": {
|
|
397
|
+
"type": "string"
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"toolsBySender": {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"propertyNames": {
|
|
405
|
+
"type": "string"
|
|
406
|
+
},
|
|
407
|
+
"additionalProperties": {
|
|
408
|
+
"type": "object",
|
|
409
|
+
"additionalProperties": false,
|
|
410
|
+
"properties": {
|
|
411
|
+
"allow": {
|
|
412
|
+
"type": "array",
|
|
413
|
+
"items": {
|
|
414
|
+
"type": "string"
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
"alsoAllow": {
|
|
418
|
+
"type": "array",
|
|
419
|
+
"items": {
|
|
420
|
+
"type": "string"
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
"deny": {
|
|
424
|
+
"type": "array",
|
|
425
|
+
"items": {
|
|
426
|
+
"type": "string"
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
"skills": {
|
|
433
|
+
"type": "array",
|
|
434
|
+
"items": {
|
|
435
|
+
"type": "string"
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
"systemPrompt": {
|
|
439
|
+
"type": "string"
|
|
314
440
|
}
|
|
315
441
|
}
|
|
316
442
|
}
|
|
@@ -329,6 +455,10 @@
|
|
|
329
455
|
},
|
|
330
456
|
"description": "Chat ids the account may MANAGE (2.12.0): add/remove members, promote/demote admins, transfer ownership, export invite links; a non-empty list also unlocks createGroup. Opposite default to readChats: absent or empty means chat management is off, [\"*\"] allows every chat."
|
|
331
457
|
},
|
|
458
|
+
"discoverChats": {
|
|
459
|
+
"type": "boolean",
|
|
460
|
+
"description": "Allow the `dialogs` action (2.16.0) to list the group chats this account belongs to — id, title and type only, never direct chats and never message content. Off unless set: it is the one read that deliberately reaches past readChats, because its job is to find chats nobody has configured yet."
|
|
461
|
+
},
|
|
332
462
|
"twoFaPassword": {
|
|
333
463
|
"description": "The account's Telegram 2FA (cloud) password — needed only by transferOwnership, which Telegram guards with an SRP proof. Literal value, or a SecretRef resolved at account start-up.",
|
|
334
464
|
"anyOf": [
|
|
@@ -403,6 +533,26 @@
|
|
|
403
533
|
"help": "Chat ids the assistant may manage (create groups, add/remove members, admins, ownership, invite links). Leave empty to keep chat management off; use * to allow every chat.",
|
|
404
534
|
"advanced": true
|
|
405
535
|
},
|
|
536
|
+
"accounts.*.groups.*.tools": {
|
|
537
|
+
"label": "Group Tool Policy",
|
|
538
|
+
"help": "Tool allow/alsoAllow/deny for this group. Applies to gateway tools (message, sessions_*, cron, memory_*, …). Under CLI backends such as claude-cli the native exec/read/write tools are not filtered per group — bind the group to a separate agent for that.",
|
|
539
|
+
"advanced": true
|
|
540
|
+
},
|
|
541
|
+
"accounts.*.groups.*.toolsBySender": {
|
|
542
|
+
"label": "Group Tool Policy by Sender",
|
|
543
|
+
"help": "Per-sender tool policy inside this group. Keys use core's typed grammar: id:<telegram id>, username:<handle>, name:<display name>, or * as the fallback.",
|
|
544
|
+
"advanced": true
|
|
545
|
+
},
|
|
546
|
+
"accounts.*.groups.*.skills": {
|
|
547
|
+
"label": "Group Skills",
|
|
548
|
+
"help": "Skill allowlist for this group. Omit to inherit the agent's skills; [] means no skills in this group.",
|
|
549
|
+
"advanced": true
|
|
550
|
+
},
|
|
551
|
+
"accounts.*.groups.*.systemPrompt": {
|
|
552
|
+
"label": "Group System Prompt",
|
|
553
|
+
"help": "Trusted prompt block appended for messages from this group — the place to state what this chat is about and what stays out of it.",
|
|
554
|
+
"advanced": true
|
|
555
|
+
},
|
|
406
556
|
"accounts.*.twoFaPassword": {
|
|
407
557
|
"label": "2FA Password",
|
|
408
558
|
"help": "The account's Telegram two-step verification password. Only needed for ownership transfer.",
|
package/package.json
CHANGED