clawgram 2.3.3 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel.js +15 -1
- package/dist/helpers.js +19 -1
- package/dist/history.js +9 -0
- package/dist/normalize.js +18 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -470,6 +470,12 @@ const createChannelPlugin = (runtimes) => {
|
|
|
470
470
|
MessageSidFull: normalized.messageId,
|
|
471
471
|
Timestamp: normalized.timestamp,
|
|
472
472
|
ReplyToId: normalized.replyToMessageId,
|
|
473
|
+
// Core renders these itself as `[Replying to: "…"]` ahead of the
|
|
474
|
+
// user body — it keys off Provider being "telegram", which is set
|
|
475
|
+
// below. Without them a highlighted reply reaches the agent as
|
|
476
|
+
// bare text, and the fragment the person pointed at is lost.
|
|
477
|
+
ReplyToQuoteText: normalized.replyQuoteText,
|
|
478
|
+
ReplyToIsQuote: normalized.replyIsQuote,
|
|
473
479
|
MessageThreadId: normalized.messageThreadId,
|
|
474
480
|
NativeChannelId: normalized.chatId,
|
|
475
481
|
OriginatingChannel: "clawgram",
|
|
@@ -530,6 +536,10 @@ const createChannelPlugin = (runtimes) => {
|
|
|
530
536
|
channel: "clawgram",
|
|
531
537
|
accountId: route.accountId ?? accountId,
|
|
532
538
|
});
|
|
539
|
+
// Boundary for the transcript fallback below: only replies
|
|
540
|
+
// written after this instant may be salvaged. Same clock as
|
|
541
|
+
// the transcript writer — both live in this process.
|
|
542
|
+
const dispatchStartedAt = Date.now();
|
|
533
543
|
const dispatchResult = await dispatchBase.dispatchReplyWithBufferedBlockDispatcher({
|
|
534
544
|
ctx: ctxPayload,
|
|
535
545
|
cfg,
|
|
@@ -598,7 +608,7 @@ const createChannelPlugin = (runtimes) => {
|
|
|
598
608
|
(dispatchCounts.block ?? 0) === 0 &&
|
|
599
609
|
(dispatchCounts.final ?? 0) === 0;
|
|
600
610
|
if (nothingDelivered) {
|
|
601
|
-
const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath);
|
|
611
|
+
const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath, dispatchStartedAt);
|
|
602
612
|
// A suppressed silent reply legitimately delivers nothing, so
|
|
603
613
|
// this fallback fires right after it. Without the same check
|
|
604
614
|
// the token would be read back from the transcript and sent.
|
|
@@ -746,6 +756,10 @@ const createChannelPlugin = (runtimes) => {
|
|
|
746
756
|
SenderUsername: normalized.senderUsername,
|
|
747
757
|
SenderName: normalized.senderDisplay,
|
|
748
758
|
ReplyToId: normalized.replyToMessageId,
|
|
759
|
+
// Same reason as the group path: highlighted replies happen in
|
|
760
|
+
// direct messages too, and the fragment is not part of the text.
|
|
761
|
+
ReplyToQuoteText: normalized.replyQuoteText,
|
|
762
|
+
ReplyToIsQuote: normalized.replyIsQuote,
|
|
749
763
|
NativeChannelId: normalized.chatId,
|
|
750
764
|
},
|
|
751
765
|
deliver: async (payload) => {
|
package/dist/helpers.js
CHANGED
|
@@ -110,7 +110,17 @@ function resolveTranscriptPathFromStoreEntry(input) {
|
|
|
110
110
|
return undefined;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Salvages a reply that reached the transcript but not stdout.
|
|
115
|
+
*
|
|
116
|
+
* `notBeforeMs` is the start of the current dispatch. Only entries stamped at
|
|
117
|
+
* or after it count: on a turn that aborted with zero output, the newest
|
|
118
|
+
* assistant entry is by definition from an earlier turn, and re-sending it
|
|
119
|
+
* addresses an old answer to a new question (the 2026-08-06 duplicates,
|
|
120
|
+
* note 0054 in the control repo). An entry with no parseable timestamp cannot
|
|
121
|
+
* prove it is fresh, so it does not count either.
|
|
122
|
+
*/
|
|
123
|
+
function readLatestAssistantFallbackFromTranscript(sessionKey, storePath, notBeforeMs) {
|
|
114
124
|
if (!storePath?.trim()) {
|
|
115
125
|
return undefined;
|
|
116
126
|
}
|
|
@@ -135,6 +145,14 @@ function readLatestAssistantFallbackFromTranscript(sessionKey, storePath) {
|
|
|
135
145
|
if (entry?.type !== "message" || entry?.message?.role !== "assistant" || !Array.isArray(entry.message.content)) {
|
|
136
146
|
continue;
|
|
137
147
|
}
|
|
148
|
+
if (notBeforeMs !== undefined) {
|
|
149
|
+
const stamped = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
|
|
150
|
+
if (!Number.isFinite(stamped) || stamped < notBeforeMs) {
|
|
151
|
+
// Entries are appended in order; everything below this one is
|
|
152
|
+
// older still. Stop instead of walking further into the past.
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
138
156
|
const textPart = entry.message.content.find((part) => part?.type === "text" && typeof part.text === "string" && part.text.trim());
|
|
139
157
|
if (!textPart?.text) {
|
|
140
158
|
continue;
|
package/dist/history.js
CHANGED
|
@@ -227,6 +227,14 @@ function resolveSenderDisplay(msg) {
|
|
|
227
227
|
return joined;
|
|
228
228
|
return readSender(msg, "title") ?? readSender(msg, "username");
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Same rule as the inbound path (`normalize.ts`): the text is the evidence,
|
|
232
|
+
* not the `quote` flag, and blank text means no highlight.
|
|
233
|
+
*/
|
|
234
|
+
function resolveHistoryReplyQuote(msg) {
|
|
235
|
+
const raw = msg?.replyTo?.quoteText ?? msg?.quoteText;
|
|
236
|
+
return typeof raw === "string" && raw.trim() ? raw : undefined;
|
|
237
|
+
}
|
|
230
238
|
/**
|
|
231
239
|
* Deliberately drops `raw`. Inbound events carry it because the runtime may need
|
|
232
240
|
* the original object; history is read straight into a model's context, where an
|
|
@@ -258,6 +266,7 @@ function normalizeHistoryMessage(msg, fallbackChatId) {
|
|
|
258
266
|
timestamp,
|
|
259
267
|
sentAt: timestamp === undefined ? undefined : new Date(timestamp * 1000).toISOString(),
|
|
260
268
|
replyToMessageId: toStringId(msg?.replyTo?.replyToMsgId) ?? toStringId(msg?.replyToMsgId),
|
|
269
|
+
replyQuoteText: resolveHistoryReplyQuote(msg),
|
|
261
270
|
messageThreadId: toStringId(msg?.replyTo?.replyToTopId) ?? toStringId(msg?.replyToTopId),
|
|
262
271
|
media: (0, media_1.describeMedia)(msg?.media),
|
|
263
272
|
isOutgoing: msg?.out === true,
|
package/dist/normalize.js
CHANGED
|
@@ -66,6 +66,21 @@ function resolveMessageThreadId(msg) {
|
|
|
66
66
|
return (toStringId(msg?.replyTo?.replyToMsgId) ??
|
|
67
67
|
toStringId(msg?.replyToMsgId));
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The highlighted fragment of a reply, when there is one.
|
|
71
|
+
*
|
|
72
|
+
* Telegram sends the flag and the text separately, and clients in the wild are
|
|
73
|
+
* not consistent about the flag. The text is the evidence: if `quoteText` has
|
|
74
|
+
* content, a fragment was highlighted regardless of what `quote` says. Blank
|
|
75
|
+
* text is treated as no highlight rather than as an empty one.
|
|
76
|
+
*/
|
|
77
|
+
function resolveReplyQuote(msg) {
|
|
78
|
+
const raw = msg?.replyTo?.quoteText ?? msg?.quoteText;
|
|
79
|
+
const text = typeof raw === "string" && raw.trim() ? raw : undefined;
|
|
80
|
+
if (!text)
|
|
81
|
+
return {};
|
|
82
|
+
return { text, isQuote: true };
|
|
83
|
+
}
|
|
69
84
|
function normalizeTelegramEvent(event, accountId) {
|
|
70
85
|
const msg = event?.message;
|
|
71
86
|
if (!msg)
|
|
@@ -84,6 +99,7 @@ function normalizeTelegramEvent(event, accountId) {
|
|
|
84
99
|
const replyToMessageId = toStringId(msg.replyTo?.replyToMsgId) ??
|
|
85
100
|
toStringId(msg.replyToMsgId);
|
|
86
101
|
const messageThreadId = resolveMessageThreadId(msg);
|
|
102
|
+
const replyQuote = resolveReplyQuote(msg);
|
|
87
103
|
const text = typeof msg.message === "string"
|
|
88
104
|
? msg.message
|
|
89
105
|
: typeof msg.text === "string"
|
|
@@ -116,6 +132,8 @@ function normalizeTelegramEvent(event, accountId) {
|
|
|
116
132
|
messageId,
|
|
117
133
|
text,
|
|
118
134
|
replyToMessageId,
|
|
135
|
+
replyQuoteText: replyQuote.text,
|
|
136
|
+
replyIsQuote: replyQuote.isQuote,
|
|
119
137
|
chatType,
|
|
120
138
|
timestamp: toTimestamp(msg.date),
|
|
121
139
|
isOutgoing: Boolean(msg.out),
|
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.4.1",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED