clawgram 2.29.1 → 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/actions-read.js +1 -0
- package/dist/attachments.js +1 -0
- package/dist/channel.js +4 -0
- package/dist/inbound-pipeline.js +29 -4
- package/dist/media.js +76 -7
- 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/actions-read.js
CHANGED
|
@@ -132,6 +132,7 @@ async function handleReadAction(ctx) {
|
|
|
132
132
|
? node_path_1.default.join((0, state_dir_1.resolveStateDir)(), "tmp")
|
|
133
133
|
: node_path_1.default.join(node_os_1.default.tmpdir(), `clawgram-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
|
|
134
134
|
await (0, media_1.ensurePrivateDir)(mediaRoot);
|
|
135
|
+
await (0, media_1.sweepOrphanMediaDirs)(mediaRoot, Date.now());
|
|
135
136
|
const sharedFetchDir = node_path_1.default.join(mediaRoot, "clawgram-fetched");
|
|
136
137
|
const described = (0, media_1.describeMedia)(found.message?.media);
|
|
137
138
|
// A PDF is not read here: the agent is handed the path for its PDF tool,
|
package/dist/attachments.js
CHANGED
|
@@ -81,6 +81,7 @@ async function readInboundAttachment(params) {
|
|
|
81
81
|
if (!media || !message) {
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
|
+
await (0, media_1.sweepOrphanMediaDirs)(node_os_1.default.tmpdir(), Date.now());
|
|
84
85
|
let downloaded;
|
|
85
86
|
try {
|
|
86
87
|
downloaded = await (0, media_1.downloadInboundMediaToTempFile)({
|
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
|
});
|
package/dist/media.js
CHANGED
|
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ORPHAN_MEDIA_DIR_TTL_MS = void 0;
|
|
18
19
|
exports.describeMedia = describeMedia;
|
|
19
20
|
exports.inboundMediaUnderstanding = inboundMediaUnderstanding;
|
|
20
21
|
exports.fetchMediaUnderstanding = fetchMediaUnderstanding;
|
|
@@ -23,6 +24,8 @@ exports.describePrivateDirProblem = describePrivateDirProblem;
|
|
|
23
24
|
exports.ensurePrivateDir = ensurePrivateDir;
|
|
24
25
|
exports.downloadMessageMediaToFile = downloadMessageMediaToFile;
|
|
25
26
|
exports.pruneFetchedMedia = pruneFetchedMedia;
|
|
27
|
+
exports.pruneOrphanMediaDirs = pruneOrphanMediaDirs;
|
|
28
|
+
exports.sweepOrphanMediaDirs = sweepOrphanMediaDirs;
|
|
26
29
|
exports.isLocalMediaPath = isLocalMediaPath;
|
|
27
30
|
exports.assertLocalMediaWithinRoots = assertLocalMediaWithinRoots;
|
|
28
31
|
exports.loadOutboundMedia = loadOutboundMedia;
|
|
@@ -167,17 +170,32 @@ async function downloadInboundMediaToTempFile(params) {
|
|
|
167
170
|
return undefined;
|
|
168
171
|
}
|
|
169
172
|
const dir = await mkdtemp(join(params.tmpDir, "clawgram-media-"));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
173
|
+
// The caller removes only a path it was handed. Every other way out — an
|
|
174
|
+
// empty download, a document, a throw — removes the directory here, or it
|
|
175
|
+
// stays in the temp directory for good (audit r3 V1-10, the inbound path).
|
|
176
|
+
const discard = async () => {
|
|
177
|
+
const { rm } = await import("node:fs/promises");
|
|
178
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
179
|
+
};
|
|
180
|
+
let downloaded;
|
|
181
|
+
try {
|
|
182
|
+
downloaded = await downloadMessageMediaToFile({
|
|
183
|
+
client: params.client,
|
|
184
|
+
message: params.message,
|
|
185
|
+
maxBytes: params.maxBytes,
|
|
186
|
+
dir,
|
|
187
|
+
fileNameFor: ({ extension }) => `attachment.${extension}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
await discard();
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
177
194
|
// This path asks only for `inboundMediaUnderstanding`, so a document cannot
|
|
178
195
|
// get here. Keep the runtime guard as the public downloader also serves the
|
|
179
196
|
// explicit `fetch-media` action, which is allowed to request DOCX files.
|
|
180
197
|
if (!downloaded || downloaded.understanding === "document" || downloaded.understanding === "pdf") {
|
|
198
|
+
await discard();
|
|
181
199
|
return undefined;
|
|
182
200
|
}
|
|
183
201
|
return {
|
|
@@ -335,6 +353,57 @@ async function pruneFetchedMedia(dir, maxAgeMs, now) {
|
|
|
335
353
|
}
|
|
336
354
|
return removed;
|
|
337
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* A private `clawgram-media-*` directory lives for one call: the inbound
|
|
358
|
+
* reader and `fetch-media mode=read` remove it when they are done. One that is
|
|
359
|
+
* an hour old was left by a crash, or by a version before 2.29.1, which kept a
|
|
360
|
+
* directory on every error branch and every PDF read (audit r3 C2-02, V1-10).
|
|
361
|
+
*/
|
|
362
|
+
exports.ORPHAN_MEDIA_DIR_TTL_MS = 60 * 60 * 1000;
|
|
363
|
+
const ORPHAN_SWEEP_EVERY_MS = 10 * 60 * 1000;
|
|
364
|
+
const lastOrphanSweep = new Map();
|
|
365
|
+
/** Removes `clawgram-media-*` directories in `root` older than `maxAgeMs`. */
|
|
366
|
+
async function pruneOrphanMediaDirs(root, maxAgeMs, now) {
|
|
367
|
+
const { readdir, stat, rm } = await import("node:fs/promises");
|
|
368
|
+
const { join } = await import("node:path");
|
|
369
|
+
let entries;
|
|
370
|
+
try {
|
|
371
|
+
entries = await readdir(root);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
let removed = 0;
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
if (!entry.startsWith("clawgram-media-"))
|
|
379
|
+
continue;
|
|
380
|
+
const path = join(root, entry);
|
|
381
|
+
try {
|
|
382
|
+
const info = await stat(path);
|
|
383
|
+
// In a shared /tmp another account's directory fails `rm` on the
|
|
384
|
+
// sticky bit and is skipped; only our own leftovers go.
|
|
385
|
+
if (info.isDirectory() && now - info.mtimeMs > maxAgeMs) {
|
|
386
|
+
await rm(path, { recursive: true, force: true });
|
|
387
|
+
removed += 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Gone already, or not ours.
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return removed;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* `pruneOrphanMediaDirs` at most once per ten minutes per root: it reads the
|
|
398
|
+
* whole temp directory, and it runs on the path of every inbound attachment.
|
|
399
|
+
*/
|
|
400
|
+
async function sweepOrphanMediaDirs(root, now) {
|
|
401
|
+
const last = lastOrphanSweep.get(root);
|
|
402
|
+
if (last !== undefined && now - last < ORPHAN_SWEEP_EVERY_MS)
|
|
403
|
+
return 0;
|
|
404
|
+
lastOrphanSweep.set(root, now);
|
|
405
|
+
return pruneOrphanMediaDirs(root, exports.ORPHAN_MEDIA_DIR_TTL_MS, now).catch(() => 0);
|
|
406
|
+
}
|
|
338
407
|
function extensionFor(media, understanding) {
|
|
339
408
|
if (understanding === "description") {
|
|
340
409
|
if (media.mimeType === "image/png")
|
|
@@ -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