clawgram 2.28.0 → 2.28.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/actions-read.js +9 -5
- package/dist/attachments.js +12 -0
- package/dist/channel.js +2 -2
- package/dist/docx-text.js +153 -0
- package/dist/media.js +39 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/actions-read.js
CHANGED
|
@@ -141,11 +141,13 @@ async function handleReadAction(ctx) {
|
|
|
141
141
|
else {
|
|
142
142
|
await (0, media_1.pruneFetchedMedia)(sharedFetchDir, FETCHED_MEDIA_TTL_MS, Date.now());
|
|
143
143
|
}
|
|
144
|
+
const described = (0, media_1.describeMedia)(found.message?.media);
|
|
144
145
|
const downloaded = await (0, media_1.downloadMessageMediaToFile)({
|
|
145
146
|
client: fetchGram.getClient(),
|
|
146
147
|
message: found.message,
|
|
147
148
|
maxBytes: attachments_1.INBOUND_MEDIA_MAX_BYTES,
|
|
148
149
|
dir: fetchDir,
|
|
150
|
+
understanding: (0, media_1.fetchMediaUnderstanding)(described),
|
|
149
151
|
fileNameFor: ({ media, extension }) => (0, fetch_media_1.fetchedMediaFileName)({
|
|
150
152
|
chatId: fetchChatId,
|
|
151
153
|
messageId: fetchParams.messageId,
|
|
@@ -159,7 +161,6 @@ async function handleReadAction(ctx) {
|
|
|
159
161
|
// channel does not read (a video, a spreadsheet), and one too
|
|
160
162
|
// large to be worth the transfer. Saying "could not fetch" to all
|
|
161
163
|
// three is how "she ignored the picture" starts.
|
|
162
|
-
const described = (0, media_1.describeMedia)(found.message?.media);
|
|
163
164
|
const tooLarge = typeof described?.size === "number" && described.size > attachments_1.INBOUND_MEDIA_MAX_BYTES;
|
|
164
165
|
const error = !described
|
|
165
166
|
? "no-media"
|
|
@@ -192,6 +193,7 @@ async function handleReadAction(ctx) {
|
|
|
192
193
|
filePath: downloaded.path,
|
|
193
194
|
mimeType: downloaded.mimeType,
|
|
194
195
|
understanding: downloaded.understanding,
|
|
196
|
+
fileName: downloaded.media.fileName,
|
|
195
197
|
});
|
|
196
198
|
if (!read) {
|
|
197
199
|
readError = "read-empty";
|
|
@@ -207,7 +209,8 @@ async function handleReadAction(ctx) {
|
|
|
207
209
|
// `read` mode is the inbound contract — the words, not the file — so
|
|
208
210
|
// the bytes go away with the answer. Any other mode keeps them:
|
|
209
211
|
// that is the whole point of asking for a path.
|
|
210
|
-
|
|
212
|
+
const pdfNeedsFile = downloaded.understanding === "pdf";
|
|
213
|
+
if (fetchParams.mode === "read" && !pdfNeedsFile) {
|
|
211
214
|
try {
|
|
212
215
|
const { rm } = await import("node:fs/promises");
|
|
213
216
|
await rm(fetchDir, { recursive: true, force: true });
|
|
@@ -217,6 +220,7 @@ async function handleReadAction(ctx) {
|
|
|
217
220
|
// over it would throw away a reading that already succeeded.
|
|
218
221
|
}
|
|
219
222
|
}
|
|
223
|
+
const finalReadError = pdfNeedsFile ? "use the PDF tool on filePath to read this PDF" : readError;
|
|
220
224
|
actionLog.info("clawgram fetch-media completed", {
|
|
221
225
|
accountId: fetchAccountId,
|
|
222
226
|
chatId: fetchChatId,
|
|
@@ -225,7 +229,7 @@ async function handleReadAction(ctx) {
|
|
|
225
229
|
kind: downloaded.media.kind,
|
|
226
230
|
understanding: downloaded.understanding,
|
|
227
231
|
characters: read?.length ?? 0,
|
|
228
|
-
readError:
|
|
232
|
+
readError: finalReadError ?? null,
|
|
229
233
|
});
|
|
230
234
|
return (0, core_1.jsonResult)({
|
|
231
235
|
ok: true,
|
|
@@ -235,9 +239,9 @@ async function handleReadAction(ctx) {
|
|
|
235
239
|
mode: fetchParams.mode,
|
|
236
240
|
media: downloaded.media,
|
|
237
241
|
understanding: downloaded.understanding,
|
|
238
|
-
filePath: fetchParams.mode === "read" ? undefined : downloaded.path,
|
|
242
|
+
filePath: fetchParams.mode === "read" && !pdfNeedsFile ? undefined : downloaded.path,
|
|
239
243
|
text: read,
|
|
240
|
-
readError,
|
|
244
|
+
readError: finalReadError,
|
|
241
245
|
});
|
|
242
246
|
}
|
|
243
247
|
/**
|
package/dist/attachments.js
CHANGED
|
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
15
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
16
|
const node_fs_1 = require("node:fs");
|
|
17
17
|
const media_1 = require("./media");
|
|
18
|
+
const docx_text_1 = require("./docx-text");
|
|
18
19
|
const state_dir_1 = require("./state-dir");
|
|
19
20
|
/** Attachments above this are left unread: a long recording or a huge image is
|
|
20
21
|
* a different conversation from a spoken line or a screenshot, and the
|
|
@@ -46,6 +47,16 @@ function resolveAgentDirForMedia(cfg) {
|
|
|
46
47
|
* become two different readings because two call sites drifted.
|
|
47
48
|
*/
|
|
48
49
|
async function understandAttachmentFile(params) {
|
|
50
|
+
if (params.understanding === "document") {
|
|
51
|
+
const { readFile } = await import("node:fs/promises");
|
|
52
|
+
const input = await readFile(params.filePath);
|
|
53
|
+
return (0, docx_text_1.isDocxDocument)(params.mimeType, params.fileName)
|
|
54
|
+
? (0, docx_text_1.extractDocxText)(input)
|
|
55
|
+
: (0, docx_text_1.extractPlainText)(input);
|
|
56
|
+
}
|
|
57
|
+
if (params.understanding === "pdf") {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
49
60
|
const media = params.runtime?.mediaUnderstanding;
|
|
50
61
|
if (!media)
|
|
51
62
|
return undefined;
|
|
@@ -98,6 +109,7 @@ async function readInboundAttachment(params) {
|
|
|
98
109
|
filePath: downloaded.path,
|
|
99
110
|
mimeType: downloaded.mimeType,
|
|
100
111
|
understanding: downloaded.understanding,
|
|
112
|
+
fileName: undefined,
|
|
101
113
|
});
|
|
102
114
|
if (!read) {
|
|
103
115
|
params.log?.info?.("clawgram attachment read empty", {
|
package/dist/channel.js
CHANGED
|
@@ -148,7 +148,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
148
148
|
"Use the `thread-list` 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. Name the chat with `chatId` and do not pass `target` — core refuses it for this action. `topics` is the same call under a name core does not know, and is only reachable through the gateway RPC.",
|
|
149
149
|
"Name the chat for `read` with `target`, never `chatId`: `read` is in core's own vocabulary, so core resolves the destination itself and reads only `to`/`target` — `chatId` is silently ignored and the call is refused as targetless. The chat-shaped reads next to it (`thread-list`, `channel-info`, `member-info`) are the opposite, because core does not know them; that asymmetry is core's, not a typo, and it cost 745 refused reads in the week before 2026-09-04.",
|
|
150
150
|
"Pass that `threadId` to `read` as well: without it a forum read returns every topic interleaved rather than the one that was asked about.",
|
|
151
|
-
"Use the `download-file` action to fetch the attachment on a message `read` reported. Name the chat with `chatId` and the message with `messageId`; do not pass `target` — core refuses it for this action:
|
|
151
|
+
"Use the `download-file` action to fetch the attachment on a message `read` reported. Name the chat with `chatId` and the message with `messageId`; do not pass `target` — core refuses it for this action: images and audio return a description or transcript; DOCX and UTF-8 text documents (.txt, .md, .csv, .json, YAML, XML, HTML, RTF) return text; PDFs return a retained `filePath` for the PDF tool. `mode: \"file\"` returns a path to reuse and `\"both\"` (default) returns both. `read` only says an attachment exists; this is what brings it.",
|
|
152
152
|
"Use the `channel-list` 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`.",
|
|
153
153
|
"Use `member-info` with a `chatId` to list who is in a chat, and `kick` with a `chatId` and `userId` to remove someone from a managed chat. The rest of the chat-management family and `joins` have no name core knows, so they are reachable only through the gateway RPC, not from this tool.",
|
|
154
154
|
"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.",
|
|
@@ -160,7 +160,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
160
160
|
"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.",
|
|
161
161
|
"clawgram can describe a chat via `channel-info`: title, type (direct/group/supergroup/channel), member count, description, whether it is a forum, and the pinned message id.",
|
|
162
162
|
"clawgram can list the topics of a forum supergroup via `thread-list`: id, title, last message, and whether a topic is closed, hidden or pinned.",
|
|
163
|
-
"clawgram can fetch
|
|
163
|
+
"clawgram can fetch an explicitly named attachment inside its read scope via `download-file`: images come back described, voice notes transcribed, DOCX and UTF-8 text documents as text, and PDFs as a private file path for the PDF tool.",
|
|
164
164
|
"clawgram can list the group chats the account belongs to via `channel-list`, when the account sets discoverChats. Metadata only, no direct chats — it answers \"where am I\", not \"what was said\".",
|
|
165
165
|
"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.",
|
|
166
166
|
],
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractDocxText = extractDocxText;
|
|
4
|
+
exports.isDocxDocument = isDocxDocument;
|
|
5
|
+
exports.isTextDocument = isTextDocument;
|
|
6
|
+
exports.extractPlainText = extractPlainText;
|
|
7
|
+
const node_zlib_1 = require("node:zlib");
|
|
8
|
+
const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
|
9
|
+
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
|
10
|
+
const LOCAL_FILE_SIGNATURE = 0x04034b50;
|
|
11
|
+
const WORD_DOCUMENT_PATH = "word/document.xml";
|
|
12
|
+
const MAX_DOCUMENT_XML_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
const TEXT_DOCUMENT_EXTENSIONS = new Set([
|
|
14
|
+
"txt", "md", "markdown", "csv", "tsv", "json", "jsonl", "yaml", "yml",
|
|
15
|
+
"toml", "ini", "cfg", "conf", "xml", "html", "htm", "log", "rtf",
|
|
16
|
+
]);
|
|
17
|
+
function findEndOfCentralDirectory(input) {
|
|
18
|
+
// The optional ZIP comment is at most 65,535 bytes, so scanning this tail
|
|
19
|
+
// avoids treating a matching four-byte sequence in compressed data as a
|
|
20
|
+
// directory record.
|
|
21
|
+
const start = Math.max(0, input.length - 65_557);
|
|
22
|
+
for (let offset = input.length - 22; offset >= start; offset -= 1) {
|
|
23
|
+
if (input.readUInt32LE(offset) === END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
|
|
24
|
+
return offset;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
28
|
+
}
|
|
29
|
+
function wordDocumentEntry(input) {
|
|
30
|
+
if (input.length < 22) {
|
|
31
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
32
|
+
}
|
|
33
|
+
const end = findEndOfCentralDirectory(input);
|
|
34
|
+
const entryCount = input.readUInt16LE(end + 10);
|
|
35
|
+
let offset = input.readUInt32LE(end + 16);
|
|
36
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
37
|
+
if (offset + 46 > input.length || input.readUInt32LE(offset) !== CENTRAL_DIRECTORY_SIGNATURE) {
|
|
38
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
39
|
+
}
|
|
40
|
+
const flags = input.readUInt16LE(offset + 8);
|
|
41
|
+
const compressionMethod = input.readUInt16LE(offset + 10);
|
|
42
|
+
const compressedSize = input.readUInt32LE(offset + 20);
|
|
43
|
+
const uncompressedSize = input.readUInt32LE(offset + 24);
|
|
44
|
+
const nameLength = input.readUInt16LE(offset + 28);
|
|
45
|
+
const extraLength = input.readUInt16LE(offset + 30);
|
|
46
|
+
const commentLength = input.readUInt16LE(offset + 32);
|
|
47
|
+
const localHeaderOffset = input.readUInt32LE(offset + 42);
|
|
48
|
+
const nameEnd = offset + 46 + nameLength;
|
|
49
|
+
if (nameEnd > input.length) {
|
|
50
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
51
|
+
}
|
|
52
|
+
const name = input.subarray(offset + 46, nameEnd).toString("utf8");
|
|
53
|
+
if (name === WORD_DOCUMENT_PATH) {
|
|
54
|
+
if ((flags & 0x1) !== 0) {
|
|
55
|
+
throw new Error("clawgram: encrypted DOCX attachments are not supported");
|
|
56
|
+
}
|
|
57
|
+
return { compressionMethod, compressedSize, uncompressedSize, localHeaderOffset };
|
|
58
|
+
}
|
|
59
|
+
offset = nameEnd + extraLength + commentLength;
|
|
60
|
+
}
|
|
61
|
+
throw new Error("clawgram: DOCX has no word/document.xml");
|
|
62
|
+
}
|
|
63
|
+
function decodeXmlText(value) {
|
|
64
|
+
return value
|
|
65
|
+
.replaceAll("&", "&")
|
|
66
|
+
.replaceAll("<", "<")
|
|
67
|
+
.replaceAll(">", ">")
|
|
68
|
+
.replaceAll(""", '"')
|
|
69
|
+
.replaceAll("'", "'")
|
|
70
|
+
.replace(/&#(x[0-9a-f]+|\d+);/gi, (_whole, source) => {
|
|
71
|
+
const codePoint = String(source).toLowerCase().startsWith("x")
|
|
72
|
+
? Number.parseInt(String(source).slice(1), 16)
|
|
73
|
+
: Number.parseInt(String(source), 10);
|
|
74
|
+
const valid = Number.isInteger(codePoint)
|
|
75
|
+
&& codePoint >= 0
|
|
76
|
+
&& codePoint <= 0x10ffff
|
|
77
|
+
&& (codePoint < 0xd800 || codePoint > 0xdfff);
|
|
78
|
+
return valid ? String.fromCodePoint(codePoint) : "";
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function textFromWordXml(xml) {
|
|
82
|
+
const withBreaks = xml
|
|
83
|
+
.replace(/<w:tab\b[^>]*\/>/gi, "\t")
|
|
84
|
+
.replace(/<w:br\b[^>]*\/>/gi, "\n")
|
|
85
|
+
.replace(/<w:cr\b[^>]*\/>/gi, "\n")
|
|
86
|
+
.replace(/<\/w:p>/gi, "\n")
|
|
87
|
+
.replace(/<[^>]*>/g, "");
|
|
88
|
+
return decodeXmlText(withBreaks)
|
|
89
|
+
.split("\n")
|
|
90
|
+
.map((line) => line.replace(/[ \t]+/g, " ").trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join("\n")
|
|
93
|
+
.trim();
|
|
94
|
+
}
|
|
95
|
+
/** Extracts plain text from the DOCX part the user explicitly asked to read. */
|
|
96
|
+
function extractDocxText(input) {
|
|
97
|
+
const entry = wordDocumentEntry(input);
|
|
98
|
+
if (entry.uncompressedSize > MAX_DOCUMENT_XML_BYTES) {
|
|
99
|
+
throw new Error("clawgram: DOCX text is too large to read");
|
|
100
|
+
}
|
|
101
|
+
const local = entry.localHeaderOffset;
|
|
102
|
+
if (local + 30 > input.length || input.readUInt32LE(local) !== LOCAL_FILE_SIGNATURE) {
|
|
103
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
104
|
+
}
|
|
105
|
+
const nameLength = input.readUInt16LE(local + 26);
|
|
106
|
+
const extraLength = input.readUInt16LE(local + 28);
|
|
107
|
+
const start = local + 30 + nameLength + extraLength;
|
|
108
|
+
const end = start + entry.compressedSize;
|
|
109
|
+
if (start > input.length || end > input.length) {
|
|
110
|
+
throw new Error("clawgram: attachment is not a valid DOCX zip");
|
|
111
|
+
}
|
|
112
|
+
const compressed = input.subarray(start, end);
|
|
113
|
+
const xml = entry.compressionMethod === 0
|
|
114
|
+
? compressed
|
|
115
|
+
: entry.compressionMethod === 8
|
|
116
|
+
? (0, node_zlib_1.inflateRawSync)(compressed, { maxOutputLength: MAX_DOCUMENT_XML_BYTES })
|
|
117
|
+
: undefined;
|
|
118
|
+
if (!xml) {
|
|
119
|
+
throw new Error(`clawgram: DOCX compression method ${entry.compressionMethod} is not supported`);
|
|
120
|
+
}
|
|
121
|
+
return textFromWordXml(xml.toString("utf8"));
|
|
122
|
+
}
|
|
123
|
+
function isDocxDocument(mimeType, fileName) {
|
|
124
|
+
return mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
125
|
+
|| fileName?.toLowerCase().endsWith(".docx") === true;
|
|
126
|
+
}
|
|
127
|
+
function isTextDocument(mimeType, fileName) {
|
|
128
|
+
if (mimeType?.startsWith("text/"))
|
|
129
|
+
return true;
|
|
130
|
+
const extension = fileName?.trim().split(".").pop()?.toLowerCase();
|
|
131
|
+
return extension ? TEXT_DOCUMENT_EXTENSIONS.has(extension) : false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Plain-text attachments are kept as UTF-8. They are never silently decoded
|
|
135
|
+
* as binary data, because replacement glyphs hide a wrong file type from the
|
|
136
|
+
* agent and make its answer look trustworthy when it is not.
|
|
137
|
+
*/
|
|
138
|
+
function extractPlainText(input) {
|
|
139
|
+
if (input.length > MAX_DOCUMENT_XML_BYTES) {
|
|
140
|
+
throw new Error("clawgram: text document is too large to read");
|
|
141
|
+
}
|
|
142
|
+
if (input.includes(0)) {
|
|
143
|
+
throw new Error("clawgram: attachment is binary, not a text document");
|
|
144
|
+
}
|
|
145
|
+
let text;
|
|
146
|
+
try {
|
|
147
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(input);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error("clawgram: text attachment is not valid UTF-8");
|
|
151
|
+
}
|
|
152
|
+
return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim();
|
|
153
|
+
}
|
package/dist/media.js
CHANGED
|
@@ -17,6 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.describeMedia = describeMedia;
|
|
19
19
|
exports.inboundMediaUnderstanding = inboundMediaUnderstanding;
|
|
20
|
+
exports.fetchMediaUnderstanding = fetchMediaUnderstanding;
|
|
20
21
|
exports.downloadInboundMediaToTempFile = downloadInboundMediaToTempFile;
|
|
21
22
|
exports.describePrivateDirProblem = describePrivateDirProblem;
|
|
22
23
|
exports.ensurePrivateDir = ensurePrivateDir;
|
|
@@ -125,6 +126,26 @@ function inboundMediaUnderstanding(media) {
|
|
|
125
126
|
return "description";
|
|
126
127
|
return undefined;
|
|
127
128
|
}
|
|
129
|
+
/** A document is downloaded only after the agent explicitly names its message. */
|
|
130
|
+
function fetchMediaUnderstanding(media) {
|
|
131
|
+
const inbound = inboundMediaUnderstanding(media);
|
|
132
|
+
if (inbound)
|
|
133
|
+
return inbound;
|
|
134
|
+
const fileName = media?.fileName?.toLowerCase() ?? "";
|
|
135
|
+
if (media?.kind === "document" && (media.mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
136
|
+
|| fileName.endsWith(".docx"))) {
|
|
137
|
+
return "document";
|
|
138
|
+
}
|
|
139
|
+
if (media?.kind === "document" && (media.mimeType === "application/pdf" || fileName.endsWith(".pdf"))) {
|
|
140
|
+
return "pdf";
|
|
141
|
+
}
|
|
142
|
+
if (media?.kind === "document" && (media.mimeType?.startsWith("text/")
|
|
143
|
+
|| ["txt", "md", "markdown", "csv", "tsv", "json", "jsonl", "yaml", "yml", "toml", "ini", "cfg", "conf", "xml", "html", "htm", "log", "rtf"]
|
|
144
|
+
.some((extension) => fileName.endsWith(`.${extension}`)))) {
|
|
145
|
+
return "document";
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
128
149
|
/**
|
|
129
150
|
* Downloads an inbound attachment to a temporary file.
|
|
130
151
|
*
|
|
@@ -147,13 +168,24 @@ async function downloadInboundMediaToTempFile(params) {
|
|
|
147
168
|
return undefined;
|
|
148
169
|
}
|
|
149
170
|
const dir = await mkdtemp(join(params.tmpDir, "clawgram-media-"));
|
|
150
|
-
|
|
171
|
+
const downloaded = await downloadMessageMediaToFile({
|
|
151
172
|
client: params.client,
|
|
152
173
|
message: params.message,
|
|
153
174
|
maxBytes: params.maxBytes,
|
|
154
175
|
dir,
|
|
155
176
|
fileNameFor: ({ extension }) => `attachment.${extension}`,
|
|
156
177
|
});
|
|
178
|
+
// This path asks only for `inboundMediaUnderstanding`, so a document cannot
|
|
179
|
+
// get here. Keep the runtime guard as the public downloader also serves the
|
|
180
|
+
// explicit `fetch-media` action, which is allowed to request DOCX files.
|
|
181
|
+
if (!downloaded || downloaded.understanding === "document" || downloaded.understanding === "pdf") {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
path: downloaded.path,
|
|
186
|
+
mimeType: downloaded.mimeType,
|
|
187
|
+
understanding: downloaded.understanding,
|
|
188
|
+
};
|
|
157
189
|
}
|
|
158
190
|
/**
|
|
159
191
|
* Why a directory that merely exists is not good enough.
|
|
@@ -238,7 +270,7 @@ async function ensurePrivateDir(dir) {
|
|
|
238
270
|
*/
|
|
239
271
|
async function downloadMessageMediaToFile(params) {
|
|
240
272
|
const described = describeMedia(params.message?.media);
|
|
241
|
-
const understanding = inboundMediaUnderstanding(described);
|
|
273
|
+
const understanding = params.understanding ?? inboundMediaUnderstanding(described);
|
|
242
274
|
if (!described || !understanding) {
|
|
243
275
|
return undefined;
|
|
244
276
|
}
|
|
@@ -316,6 +348,11 @@ function extensionFor(media, understanding) {
|
|
|
316
348
|
return "mp3";
|
|
317
349
|
if (media.mimeType === "audio/mp4")
|
|
318
350
|
return "m4a";
|
|
351
|
+
if (understanding === "pdf")
|
|
352
|
+
return "pdf";
|
|
353
|
+
if (understanding === "document") {
|
|
354
|
+
return media.mimeType?.startsWith("text/") ? "txt" : "docx";
|
|
355
|
+
}
|
|
319
356
|
return "ogg";
|
|
320
357
|
}
|
|
321
358
|
/**
|
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.28.
|
|
5
|
+
"version": "2.28.1",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED