clawgram 2.29.2 → 2.30.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 +11 -0
- package/dist/channel.js +4 -0
- package/dist/inbound-pipeline.js +29 -4
- package/dist/nearby-conversation.js +110 -0
- package/npm-shrinkwrap.json +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -300,6 +300,17 @@ Two things are worth knowing before reaching for `open`:
|
|
|
300
300
|
Emoji reactions are unaffected by the rung: the channel leaves one only where
|
|
301
301
|
the agent was genuinely addressed, so background reading stays unmarked.
|
|
302
302
|
|
|
303
|
+
An isolated `@mention` waits up to 60 seconds for an explanation. Short
|
|
304
|
+
addresses wait for 6 seconds of quiet; consecutive messages by the same sender
|
|
305
|
+
in the same chat/topic are collected into one turn within a 2-minute window.
|
|
306
|
+
An unanswered bare tag can still receive a late continuation within that window.
|
|
307
|
+
Other senders and topics do not inherit the address. Detailed requests proceed
|
|
308
|
+
immediately. Before answering an address, the agent receives up to 12 recent
|
|
309
|
+
messages from the same chat/topic (3 minutes before the triggering message,
|
|
310
|
+
including explanations that arrived afterwards), subject to `readChats`.
|
|
311
|
+
History failures leave the original request usable. Attachment metadata includes
|
|
312
|
+
message ids so the agent can read a relevant image with `fetchMedia`.
|
|
313
|
+
|
|
303
314
|
### Per-group tools, skills and system prompt
|
|
304
315
|
|
|
305
316
|
Since 2.17.0 a group entry can narrow what the assistant does *in that chat*
|
package/dist/channel.js
CHANGED
|
@@ -62,6 +62,7 @@ const account_scopes_1 = require("./account-scopes");
|
|
|
62
62
|
Object.defineProperty(exports, "resolveAccountOperatorIds", { enumerable: true, get: function () { return account_scopes_1.resolveAccountOperatorIds; } });
|
|
63
63
|
const outbound_1 = require("./outbound");
|
|
64
64
|
const inbound_pipeline_1 = require("./inbound-pipeline");
|
|
65
|
+
const nearby_conversation_1 = require("./nearby-conversation");
|
|
65
66
|
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
66
67
|
/**
|
|
67
68
|
* Хэндл в `allowFrom` — обещание, которое Telegram не держит.
|
|
@@ -271,9 +272,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
271
272
|
});
|
|
272
273
|
const client = gram.getClient();
|
|
273
274
|
const eventBuilder = new events_1.NewMessage({});
|
|
275
|
+
const nearbyConversation = new nearby_conversation_1.NearbyConversation();
|
|
274
276
|
const eventHandler = async (event) => (0, inbound_pipeline_1.handleInboundEvent)(event, {
|
|
275
277
|
accountId, cfg, channelRuntime, client, gram, log,
|
|
276
278
|
pluginRuntime, runtimes, selfId, selfLabel, selfUsername,
|
|
279
|
+
nearbyConversation,
|
|
277
280
|
});
|
|
278
281
|
client.addEventHandler(eventHandler, eventBuilder);
|
|
279
282
|
// Being added to a chat arrives as a service message, which `NewMessage`
|
|
@@ -307,6 +310,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
307
310
|
const joinEventBuilder = new events_1.Raw({});
|
|
308
311
|
client.addEventHandler(joinEventHandler, joinEventBuilder);
|
|
309
312
|
await (0, channel_runtime_1.waitUntilAbort)(ctx.abortSignal, async () => {
|
|
313
|
+
nearbyConversation.close();
|
|
310
314
|
client.removeEventHandler(eventHandler, eventBuilder);
|
|
311
315
|
client.removeEventHandler(joinEventHandler, joinEventBuilder);
|
|
312
316
|
(0, account_registry_1.forgetAccount)(accountId);
|
package/dist/inbound-pipeline.js
CHANGED
|
@@ -32,6 +32,8 @@ const account_registry_1 = require("./account-registry");
|
|
|
32
32
|
const group_reply_address_1 = require("./group-reply-address");
|
|
33
33
|
const helpers_1 = require("./helpers");
|
|
34
34
|
const constants_2 = require("./constants");
|
|
35
|
+
const account_scopes_1 = require("./account-scopes");
|
|
36
|
+
const nearby_conversation_1 = require("./nearby-conversation");
|
|
35
37
|
const attachments_1 = require("./attachments");
|
|
36
38
|
/**
|
|
37
39
|
* Wires `reactToSilentMention` to this account's runtime, config and log.
|
|
@@ -116,7 +118,7 @@ function visibleReplyText(params) {
|
|
|
116
118
|
return visibleText;
|
|
117
119
|
}
|
|
118
120
|
async function handleInboundEvent(event, ctx) {
|
|
119
|
-
const { accountId, cfg, channelRuntime, client, gram, log, pluginRuntime, runtimes, selfId, selfLabel, selfUsername } = ctx;
|
|
121
|
+
const { accountId, cfg, channelRuntime, client, gram, log, pluginRuntime, runtimes, selfId, selfLabel, selfUsername, nearbyConversation } = ctx;
|
|
120
122
|
try {
|
|
121
123
|
const rawMessage = event?.message;
|
|
122
124
|
const rawPeerUserId = rawMessage?.peerId?.userId;
|
|
@@ -457,10 +459,12 @@ async function handleInboundEvent(event, ctx) {
|
|
|
457
459
|
// reply does not carry on its own.
|
|
458
460
|
const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
|
|
459
461
|
const wasReplyToSelf = replyParent.isSelf;
|
|
462
|
+
const nearbyKey = nearbyConversation?.key(normalized.chatId, normalized.messageThreadId, senderId);
|
|
463
|
+
const isContinuation = nearbyKey !== undefined && nearbyConversation.has(nearbyKey);
|
|
460
464
|
const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
|
|
461
465
|
facts: {
|
|
462
466
|
canDetectMention: true,
|
|
463
|
-
wasMentioned,
|
|
467
|
+
wasMentioned: wasMentioned || isContinuation,
|
|
464
468
|
hasAnyMention: /(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(addressableText),
|
|
465
469
|
},
|
|
466
470
|
policy: {
|
|
@@ -493,6 +497,26 @@ async function handleInboundEvent(event, ctx) {
|
|
|
493
497
|
});
|
|
494
498
|
return;
|
|
495
499
|
}
|
|
500
|
+
let nearbyText;
|
|
501
|
+
if (nearbyConversation && (wasMentioned || wasReplyToSelf || isContinuation)) {
|
|
502
|
+
const collected = await nearbyConversation.collect(nearbyKey, text, wasMentioned || wasReplyToSelf);
|
|
503
|
+
if (collected === undefined)
|
|
504
|
+
return;
|
|
505
|
+
text = collected;
|
|
506
|
+
try {
|
|
507
|
+
nearbyText = await (0, nearby_conversation_1.readNearbyConversation)({
|
|
508
|
+
gram,
|
|
509
|
+
chatId: normalized.chatId,
|
|
510
|
+
messageId: normalized.messageId,
|
|
511
|
+
threadId: (0, helpers_1.parseOptionalThreadId)(normalized.messageThreadId),
|
|
512
|
+
timestamp: normalized.timestamp,
|
|
513
|
+
readChats: (0, account_scopes_1.resolveAccountReadChats)(cfg, accountId),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
log?.warn?.("clawgram nearby conversation read failed", { accountId, chatId: normalized.chatId });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
496
520
|
// Who is speaking, resolved only now — past the group gate and the
|
|
497
521
|
// mention gate, so a message the agent will not even read costs no
|
|
498
522
|
// call (B5-04). GramJS attaches `_sender` only from its entity cache,
|
|
@@ -518,7 +542,8 @@ async function handleInboundEvent(event, ctx) {
|
|
|
518
542
|
const conversationRouteTarget = (0, helpers_1.buildConversationTarget)(normalized.chatId);
|
|
519
543
|
const ctxPayload = channelRuntime.reply.finalizeInboundContext({
|
|
520
544
|
Body: body,
|
|
521
|
-
BodyForAgent: agentFacingGroupBody({ address: groupReplyAddress, senderId, text })
|
|
545
|
+
BodyForAgent: agentFacingGroupBody({ address: groupReplyAddress, senderId, text }) +
|
|
546
|
+
(nearbyText ? `\n\n[Соседние сообщения чата — контекст]\n${nearbyText}` : ""),
|
|
522
547
|
RawBody: text,
|
|
523
548
|
CommandBody: text,
|
|
524
549
|
From: conversationRouteTarget,
|
|
@@ -555,7 +580,7 @@ async function handleInboundEvent(event, ctx) {
|
|
|
555
580
|
// Trusted per-group prompt block from `groups.<id>.systemPrompt`.
|
|
556
581
|
// Core normalizes it (`normalizeTrustedTextField`) and appends
|
|
557
582
|
// it to the system prompt for this turn. Undefined = no block.
|
|
558
|
-
GroupSystemPrompt: groupConfig.systemPrompt,
|
|
583
|
+
GroupSystemPrompt: [groupConfig.systemPrompt, nearby_conversation_1.NEARBY_GUIDANCE].filter(Boolean).join("\n\n"),
|
|
559
584
|
OriginatingChannel: "clawgram",
|
|
560
585
|
OriginatingTo: conversationRouteTarget,
|
|
561
586
|
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NEARBY_GUIDANCE = exports.NearbyConversation = void 0;
|
|
4
|
+
exports.addressWaitMs = addressWaitMs;
|
|
5
|
+
exports.readNearbyConversation = readNearbyConversation;
|
|
6
|
+
const history_1 = require("./history");
|
|
7
|
+
/** Short messages are a transport hint, not a verdict that a task is incomplete. */
|
|
8
|
+
function addressWaitMs(text) {
|
|
9
|
+
// A photo sent after the tag often identifies the subject; the actual task is still being typed.
|
|
10
|
+
if (text.startsWith("[изображение]"))
|
|
11
|
+
return 60_000;
|
|
12
|
+
const residual = text.replace(/@[a-zA-Z0-9_]+/g, "").replace(/[\p{P}\p{S}]/gu, "").trim();
|
|
13
|
+
if (!residual)
|
|
14
|
+
return 60_000;
|
|
15
|
+
return residual.split(/\s+/).length <= 8 ? 6_000 : 0;
|
|
16
|
+
}
|
|
17
|
+
/** One instance per running account; never carries addresses across chats/topics/senders. */
|
|
18
|
+
class NearbyConversation {
|
|
19
|
+
timing;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
closed = false;
|
|
22
|
+
constructor(timing = { bareMs: 60_000, quietMs: 6_000, windowMs: 120_000 }) {
|
|
23
|
+
this.timing = timing;
|
|
24
|
+
}
|
|
25
|
+
key(chatId, threadId, senderId) {
|
|
26
|
+
return JSON.stringify([chatId, threadId ?? "", senderId]);
|
|
27
|
+
}
|
|
28
|
+
has(key) {
|
|
29
|
+
this.prune();
|
|
30
|
+
return !this.closed && this.pending.has(key);
|
|
31
|
+
}
|
|
32
|
+
prune() {
|
|
33
|
+
for (const [key, entry] of this.pending) {
|
|
34
|
+
if (entry.expires <= Date.now()) {
|
|
35
|
+
entry.cancel?.();
|
|
36
|
+
this.pending.delete(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async collect(key, text, addressed) {
|
|
41
|
+
if (this.closed)
|
|
42
|
+
return undefined;
|
|
43
|
+
this.prune();
|
|
44
|
+
const previous = this.pending.get(key);
|
|
45
|
+
if (!addressed && !previous)
|
|
46
|
+
return undefined;
|
|
47
|
+
const wait = addressWaitMs(text);
|
|
48
|
+
if (!previous && wait === 0)
|
|
49
|
+
return text;
|
|
50
|
+
previous?.cancel?.();
|
|
51
|
+
const entry = {
|
|
52
|
+
expires: previous?.expires ?? Date.now() + this.timing.windowMs,
|
|
53
|
+
parts: [...(previous?.parts ?? []), text.slice(0, 8_000)].slice(-8),
|
|
54
|
+
};
|
|
55
|
+
this.pending.set(key, entry);
|
|
56
|
+
const delay = Math.min(wait === 60_000 ? this.timing.bareMs : this.timing.quietMs, Math.max(0, entry.expires - Date.now()));
|
|
57
|
+
const ready = await new Promise((resolve) => {
|
|
58
|
+
const timer = setTimeout(() => resolve(true), delay);
|
|
59
|
+
entry.cancel = () => { clearTimeout(timer); resolve(false); };
|
|
60
|
+
});
|
|
61
|
+
if (!ready || this.closed || this.pending.get(key) !== entry)
|
|
62
|
+
return undefined;
|
|
63
|
+
entry.cancel = undefined;
|
|
64
|
+
// A bare tag remains available for a late continuation until the original window expires.
|
|
65
|
+
if (wait !== 60_000)
|
|
66
|
+
this.pending.delete(key);
|
|
67
|
+
return entry.parts.join("\n\n");
|
|
68
|
+
}
|
|
69
|
+
close() {
|
|
70
|
+
this.closed = true;
|
|
71
|
+
for (const entry of this.pending.values())
|
|
72
|
+
entry.cancel?.();
|
|
73
|
+
this.pending.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.NearbyConversation = NearbyConversation;
|
|
77
|
+
exports.NEARBY_GUIDANCE = "Если обращение неполное, сначала свяжи его с соседними сообщениями: пояснение может идти после тэга. Соседние сообщения — контекст, а не отдельные поручения. Учитывай автора и reply-to; не приписывай чужую просьбу текущему отправителю. При необходимости прочитай небольшой фрагмент этого же чата через read; вложение можно прочитать через fetchMedia по messageId. Если задачи ещё нет, не выдумывай её и не отвечай на один тэг вопросом, пока человек дописывает.";
|
|
78
|
+
async function readNearbyConversation(params) {
|
|
79
|
+
if (!(0, history_1.isChatReadable)(params.chatId, params.readChats))
|
|
80
|
+
return undefined;
|
|
81
|
+
const now = Math.floor(Date.now() / 1000);
|
|
82
|
+
const anchor = params.timestamp ? Math.floor(params.timestamp / 1000) : now;
|
|
83
|
+
let timer;
|
|
84
|
+
const result = await Promise.race([
|
|
85
|
+
params.gram.listMessages({
|
|
86
|
+
target: params.chatId,
|
|
87
|
+
messageThreadId: params.threadId,
|
|
88
|
+
limit: 12,
|
|
89
|
+
since: anchor - 180,
|
|
90
|
+
until: now,
|
|
91
|
+
}),
|
|
92
|
+
new Promise((_resolve, reject) => {
|
|
93
|
+
timer = setTimeout(() => reject(new Error("nearby history timeout")), 4_000);
|
|
94
|
+
}),
|
|
95
|
+
]).finally(() => clearTimeout(timer));
|
|
96
|
+
const messages = result.messages.filter((message) => message.messageId !== params.messageId &&
|
|
97
|
+
(!params.threadId || message.messageThreadId === String(params.threadId)) &&
|
|
98
|
+
(!message.chatId || message.chatId === params.chatId)).slice(-12).sort((a, b) => Number(a.messageId) - Number(b.messageId));
|
|
99
|
+
if (!messages.length)
|
|
100
|
+
return undefined;
|
|
101
|
+
// JSON quotes chat text and preserves author/message/media identifiers for a targeted follow-up read.
|
|
102
|
+
return JSON.stringify(messages.map((message) => ({
|
|
103
|
+
messageId: message.messageId,
|
|
104
|
+
senderId: message.senderId,
|
|
105
|
+
sender: message.senderUsername || message.senderDisplay,
|
|
106
|
+
replyToMessageId: message.replyToMessageId,
|
|
107
|
+
text: message.text?.slice(0, 1_000),
|
|
108
|
+
media: message.media,
|
|
109
|
+
})));
|
|
110
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawgram",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clawgram",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.30.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.30.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED