clawgram 2.21.1 → 2.23.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 +34 -8
- package/dist/actions.js +124 -0
- package/dist/attachments.js +142 -0
- package/dist/channel.js +426 -720
- package/dist/chat-info.js +28 -22
- package/dist/cli-core.js +109 -20
- package/dist/expiring-map.js +96 -0
- package/dist/gramjs-client.js +104 -29
- package/dist/group-reply-address.js +6 -16
- package/dist/group-visible-reply-guard.js +12 -32
- package/dist/helpers.js +71 -5
- package/dist/history.js +54 -22
- package/dist/html-render.js +44 -1
- package/dist/joins.js +8 -6
- package/dist/manage.js +9 -15
- package/dist/media.js +15 -16
- package/dist/normalize.js +6 -1
- package/dist/outbound.js +260 -0
- package/dist/proxy-config.js +2 -4
- package/dist/reactions.js +3 -12
- package/dist/secret-refs.js +1 -0
- package/dist/send-scope.js +97 -0
- package/dist/silent-reaction.js +9 -5
- package/dist/state-dir.js +24 -0
- package/dist/system-notice.js +49 -4
- package/dist/update-config.js +139 -33
- package/dist/util.js +36 -0
- package/npm-shrinkwrap.json +4531 -0
- package/openclaw.plugin.json +39 -5
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ Clawgram is a personal-Telegram channel plugin for [OpenClaw](https://github.com
|
|
|
18
18
|
> it can read private and group conversations within its configured scope and send messages that are
|
|
19
19
|
> indistinguishable from ones you typed yourself. Recipients cannot tell an assistant reply from a
|
|
20
20
|
> human one. Treat the account as compromised-if-leaked: `apiHash` and `sessionString` are bearer
|
|
21
|
-
> credentials for everything that account can reach. Scope reads with `readChats`,
|
|
22
|
-
> `allowFrom`, and prefer a dedicated account over your primary one —
|
|
23
|
-
> [Security and privacy](#security-and-privacy).
|
|
21
|
+
> credentials for everything that account can reach. Scope reads with `readChats`, scope sends with
|
|
22
|
+
> `sendChats`, gate senders with `allowFrom`, and prefer a dedicated account over your primary one —
|
|
23
|
+
> see [Security and privacy](#security-and-privacy).
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
@@ -184,6 +184,13 @@ openclaw gateway restart
|
|
|
184
184
|
openclaw gateway restart
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
**What `--auth` writes is closed, on purpose.** The first config allows only the
|
|
188
|
+
account that authorised (`allowFrom: [<your id>]`), seeds no group entry, and
|
|
189
|
+
writes `readChats: []`. Until 2.21.1 it seeded `allowFrom: ["*"]` with an
|
|
190
|
+
enabled wildcard group, so a freshly authorised account answered any Telegram
|
|
191
|
+
user and read the history of every chat it belonged to. Widen both lists
|
|
192
|
+
deliberately; the flow prints who can reach the agent before it exits.
|
|
193
|
+
|
|
187
194
|
One restart after installation is enough. From 2.17.0 the plugin declares
|
|
188
195
|
`channels.clawgram` as a hot-reloadable prefix, so later edits under it —
|
|
189
196
|
`allowFrom`, `groups`, `readChats`, proxy — are picked up by the Gateway's
|
|
@@ -249,6 +256,7 @@ loud where it does occur.
|
|
|
249
256
|
| `apiHash` | string | required | Telegram API hash |
|
|
250
257
|
| `sessionString` | string | `""` | Authenticated StringSession |
|
|
251
258
|
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames for direct messages only. Three states: absent means everyone, `[]` denies everyone (a warning is logged at account start), a list allows those senders |
|
|
259
|
+
| `operatorIds` | string[] | — | Who receives core's operational telemetry in a DM (tool-failure warnings, fallback notices). They quote shell commands and secret-store paths, so they go only to these ids. Absent falls back to `allowFrom` when it is a concrete list; a wildcard or an empty list means no operator is identified and the notices are dropped everywhere — they stay in the run diagnostics and the gateway log |
|
|
252
260
|
| `groups` | object | `{}` | Allowed groups map keyed by explicit group id or `*` |
|
|
253
261
|
| `proxy` | object | unset | Optional SOCKS4/SOCKS5 proxy for this account — see [Proxy (SOCKS4/SOCKS5)](#proxy-socks4socks5) |
|
|
254
262
|
| `manageChats` | string[] | unset | Chats the assistant may **manage** — see [Chat management](#chat-management). Absent or empty = management off; `["*"]` = every chat |
|
|
@@ -809,10 +817,27 @@ nothing is not an error — it says which nothing it was:
|
|
|
809
817
|
A reading that fails while the download succeeded still returns `ok: true`, with `readError` beside
|
|
810
818
|
the path: the bytes are already there and can still be forwarded.
|
|
811
819
|
|
|
812
|
-
**Fetched files live
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
820
|
+
**Fetched files live under the OpenClaw state directory** (`$OPENCLAW_STATE_DIR/tmp/clawgram-fetched/`,
|
|
821
|
+
falling back to the system temp directory when that variable is unset), named after the chat and
|
|
822
|
+
message they came from, and are pruned an hour later by the next fetch. The directory is created
|
|
823
|
+
0700 and each file written 0600: these are private-chat images and voice notes, and until 2.21.1
|
|
824
|
+
they sat in a shared `/tmp` at 0644 for a day, readable by every local account on the host. Nothing
|
|
825
|
+
else removes them, and nothing sends them anywhere — putting a fetched file in a chat is an ordinary
|
|
826
|
+
`upload-file`, with whatever confirmation the deployment requires for that.
|
|
827
|
+
|
|
828
|
+
## Dependencies are pinned, and the lock ships
|
|
829
|
+
|
|
830
|
+
`installDependencies: true` tells OpenClaw to run an install in the plugin
|
|
831
|
+
directory, so what lands on the host is whatever the registry serves that day —
|
|
832
|
+
and the MTProto client sees `apiHash`, `sessionString`, the proxy credentials
|
|
833
|
+
and every message. Since 2.21.1 the two runtime dependencies are pinned to
|
|
834
|
+
exact versions and `npm-shrinkwrap.json` is part of the published tarball, so an
|
|
835
|
+
install reproduces the tree the tests ran against rather than resolving a caret
|
|
836
|
+
range. `npm ci --ignore-scripts` verifies it.
|
|
837
|
+
|
|
838
|
+
The transitive `ip-address` is held at 10.7.0 through `overrides`: everything at
|
|
839
|
+
or below 10.3.0 carries three advisories about SSRF and trust-boundary bypass,
|
|
840
|
+
and it sits under the SOCKS proxy path this channel uses.
|
|
816
841
|
|
|
817
842
|
## Security and privacy
|
|
818
843
|
|
|
@@ -825,9 +850,10 @@ that means in practice, and what the code does about it:
|
|
|
825
850
|
| Proxy password | `accounts.*.proxy.password`, or a secret store | Also accepts a SecretRef since 2.2.0. Marked `sensitive` in `uiHints`; diagnostics say `socks4`/`socks5` and nothing more. An invalid proxy fails the account rather than falling back to a direct connection, which would leak the host IP to Telegram |
|
|
826
851
|
| Message bodies | channel logs | **Not logged.** Outbound sends record recipient, ids and `textLength`. Until 2.1.0 the full outbound text was written to the channel log — if you ran 2.0.x, treat those journal entries as containing private correspondence |
|
|
827
852
|
| Read scope | `accounts.*.readChats` | History, membership and attachment fetches are confined to the listed chats. Absent means no restriction; an empty array denies everything. Telegram's own service chat (`777000`, where login codes arrive) is refused unconditionally, including under a wildcard |
|
|
853
|
+
| Send scope | `accounts.*.sendChats` | `send`, `upload-file`, `react` and core's delivery path are confined to the listed chats. Same shape as `readChats`: absent means no restriction, `[]` denies everything, `["*"]` allows every chat. **A phone number is refused whatever the list says** — messaging a raw number starts a conversation with someone who never contacted the account. Without the list, an injected turn can write to strangers from the owner's account or move a work chat's content into a DM one send at a time (2.22.0) |
|
|
828
854
|
| Manage scope | `accounts.*.manageChats` | Creating groups, changing membership, admin rights, ownership and invite links are confined to the listed chats — and **off entirely** when the key is absent or empty (opposite default to `readChats`, because these actions change chats rather than read them) |
|
|
829
855
|
| 2FA password | `accounts.*.twoFaPassword`, or a secret store | Read only by `transferOwnership`, exchanged for an SRP proof in-process. Accepts a SecretRef since 2.12.0; `sensitive` in `uiHints`; on the forbidden-log-keys list the static tests enforce |
|
|
830
|
-
| Who may talk to it | `allowFrom`, `groups.*.groupPolicy` | Direct-message senders and group behaviour are allowlisted; `mention` limits group replies to explicit mentions |
|
|
856
|
+
| Who may talk to it | `allowFrom`, `groups.*.groupPolicy` | Direct-message senders and group behaviour are allowlisted; `mention` limits group replies to explicit mentions. **Write ids, not handles:** an `@username` entry is a claim about a handle, and a released handle can be taken by someone else, after which the entry admits a stranger. The account logs a warning at start-up for every handle entry (2.22.0) |
|
|
831
857
|
|
|
832
858
|
Two static tests (`test/no-secret-logging.test.ts`) fail the build if a message body or a credential
|
|
833
859
|
is ever added back to a log call, or if the auth flow prints the session string unprompted.
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Имена действий: что принимается и во что разрешается.
|
|
3
|
+
//
|
|
4
|
+
// Вынесено из channel.ts, который был единственным файлом на 3274 строки при
|
|
5
|
+
// следующем по величине 1165 (находка A6-11). Здесь нет ни рантайма, ни
|
|
6
|
+
// конфига — только словарь, поэтому читать и править его можно, не открывая
|
|
7
|
+
// диспетчер.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.MANAGE_ACTIONS = exports.CORE_ACTION_SYNONYMS = void 0;
|
|
10
|
+
exports.canonicalAction = canonicalAction;
|
|
11
|
+
/**
|
|
12
|
+
* Every accepted spelling of an action, mapped to its canonical name.
|
|
13
|
+
*
|
|
14
|
+
* One table, not three. The synonyms used to live in
|
|
15
|
+
* `CORE_ACTION_SYNONYMS`, again in `MANAGE_ACTION_ALIASES`, and a third time
|
|
16
|
+
* as `action === "…" || …` chains inside the dispatcher — and the dispatcher
|
|
17
|
+
* read only the chains. A name could therefore be added to a table and to the
|
|
18
|
+
* advertised list and still reach nothing, with the suite none the wiser:
|
|
19
|
+
* it only ever dispatched the native spellings (finding A6-10).
|
|
20
|
+
*
|
|
21
|
+
* `canonicalAction` is now the only place a name is resolved, and
|
|
22
|
+
* `CORE_ACTION_SYNONYMS` below is derived from this table rather than kept
|
|
23
|
+
* beside it.
|
|
24
|
+
*/
|
|
25
|
+
const ACTION_ALIASES = {
|
|
26
|
+
send: "send",
|
|
27
|
+
read: "read",
|
|
28
|
+
// `list` is accepted so a caller that guessed the other obvious name is not
|
|
29
|
+
// silently refused.
|
|
30
|
+
list: "read",
|
|
31
|
+
react: "react",
|
|
32
|
+
joins: "joins",
|
|
33
|
+
"upload-file": "upload-file",
|
|
34
|
+
sendAttachment: "upload-file",
|
|
35
|
+
"fetch-media": "fetch-media",
|
|
36
|
+
fetchMedia: "fetch-media",
|
|
37
|
+
"download-media": "fetch-media",
|
|
38
|
+
downloadMedia: "fetch-media",
|
|
39
|
+
getMedia: "fetch-media",
|
|
40
|
+
"download-file": "fetch-media",
|
|
41
|
+
participants: "participants",
|
|
42
|
+
members: "participants",
|
|
43
|
+
"member-info": "participants",
|
|
44
|
+
topics: "topics",
|
|
45
|
+
forumTopics: "topics",
|
|
46
|
+
"thread-list": "topics",
|
|
47
|
+
dialogs: "dialogs",
|
|
48
|
+
chats: "dialogs",
|
|
49
|
+
"channel-list": "dialogs",
|
|
50
|
+
chatInfo: "chatInfo",
|
|
51
|
+
getChatInfo: "chatInfo",
|
|
52
|
+
"channel-info": "chatInfo",
|
|
53
|
+
chatMetadata: "chatInfo",
|
|
54
|
+
getChatMetadata: "chatInfo",
|
|
55
|
+
// Chat management. `kick` was already accepted; the rest were advertised
|
|
56
|
+
// under names core does not know and were therefore never callable from the
|
|
57
|
+
// tool at all — 2.19.4 gives them core's nearest name. `transferOwnership`
|
|
58
|
+
// and `inviteLink` have no counterpart in that vocabulary and stay
|
|
59
|
+
// gateway-only, as does `joins`.
|
|
60
|
+
createGroup: "createGroup",
|
|
61
|
+
createChat: "createGroup",
|
|
62
|
+
"create-group": "createGroup",
|
|
63
|
+
"channel-create": "createGroup",
|
|
64
|
+
addMembers: "addMembers",
|
|
65
|
+
addMember: "addMembers",
|
|
66
|
+
"add-members": "addMembers",
|
|
67
|
+
addParticipant: "addMembers",
|
|
68
|
+
removeMember: "removeMember",
|
|
69
|
+
removeMembers: "removeMember",
|
|
70
|
+
"remove-member": "removeMember",
|
|
71
|
+
kick: "removeMember",
|
|
72
|
+
promoteAdmin: "promoteAdmin",
|
|
73
|
+
promote: "promoteAdmin",
|
|
74
|
+
"promote-admin": "promoteAdmin",
|
|
75
|
+
setAdmin: "promoteAdmin",
|
|
76
|
+
"role-add": "promoteAdmin",
|
|
77
|
+
demoteAdmin: "demoteAdmin",
|
|
78
|
+
demote: "demoteAdmin",
|
|
79
|
+
"demote-admin": "demoteAdmin",
|
|
80
|
+
"role-remove": "demoteAdmin",
|
|
81
|
+
transferOwnership: "transferOwnership",
|
|
82
|
+
transferOwner: "transferOwnership",
|
|
83
|
+
"transfer-ownership": "transferOwnership",
|
|
84
|
+
inviteLink: "inviteLink",
|
|
85
|
+
exportInviteLink: "inviteLink",
|
|
86
|
+
"invite-link": "inviteLink",
|
|
87
|
+
};
|
|
88
|
+
/** The canonical action for a spelling; an unknown name stays itself. */
|
|
89
|
+
function canonicalAction(action) {
|
|
90
|
+
return ACTION_ALIASES[action] ?? action;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Core's own name for a clawgram action, and the only thing that makes the
|
|
94
|
+
* action reachable from the agent's `message` tool.
|
|
95
|
+
*
|
|
96
|
+
* Core keys its target policy by `CHANNEL_MESSAGE_ACTION_NAMES`, and an action
|
|
97
|
+
* outside that vocabulary is simultaneously "requires a target" and "does not
|
|
98
|
+
* accept a target" — there is no call that satisfies both. Declaring `chatId`
|
|
99
|
+
* through `messageActionTargetAliases` looks like the fix and is not: core
|
|
100
|
+
* resolves the channel with `getBootstrapChannelPlugin`, which only knows
|
|
101
|
+
* bundled channels, so a plugin channel's declaration is never read. Measured
|
|
102
|
+
* on 2026-08-30 — `thread-list` reached `handleAction` and `topics` did not,
|
|
103
|
+
* from the same caller, on the same chat.
|
|
104
|
+
*
|
|
105
|
+
* Every name on the right maps to core target mode `"none"` except
|
|
106
|
+
* `channel-info`, which is `"channelId"`: the chat arrives in
|
|
107
|
+
* `params.channelId`, a spelling no parser here read until 2.21.0 — so the
|
|
108
|
+
* call fell through to the current chat and answered about the wrong one.
|
|
109
|
+
* `readChatTargetParam` is the single list of accepted spellings now.
|
|
110
|
+
*
|
|
111
|
+
* These spellings are derived from `ACTION_ALIASES` rather than kept beside
|
|
112
|
+
* it; that core actually knows each of them is asserted against the installed
|
|
113
|
+
* core in `core-action-synonyms.test.ts`.
|
|
114
|
+
*/
|
|
115
|
+
const CORE_VOCABULARY_SPELLINGS = [
|
|
116
|
+
"thread-list", "channel-list", "channel-info", "member-info", "download-file",
|
|
117
|
+
"channel-create", "addParticipant", "kick", "role-add", "role-remove",
|
|
118
|
+
];
|
|
119
|
+
exports.CORE_ACTION_SYNONYMS = Object.fromEntries(CORE_VOCABULARY_SPELLINGS.map((name) => [name, ACTION_ALIASES[name]]));
|
|
120
|
+
/** Canonical actions that go through the chat-management gate. */
|
|
121
|
+
exports.MANAGE_ACTIONS = new Set([
|
|
122
|
+
"createGroup", "addMembers", "removeMember",
|
|
123
|
+
"promoteAdmin", "demoteAdmin", "transferOwnership", "inviteLink",
|
|
124
|
+
]);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Входящее вложение: скачать, понять, отдать текстом.
|
|
3
|
+
//
|
|
4
|
+
// Вынесено из channel.ts — файла на 3145 строк при следующем по величине
|
|
5
|
+
// 1165 (находка A6-11). Ни диспетчера, ни конфигурации канала здесь нет:
|
|
6
|
+
// только путь «файл → текст», общий у входящего контура и у `fetch-media`.
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.INBOUND_MEDIA_MAX_BYTES = void 0;
|
|
12
|
+
exports.understandAttachmentFile = understandAttachmentFile;
|
|
13
|
+
exports.readInboundAttachment = readInboundAttachment;
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const media_1 = require("./media");
|
|
18
|
+
const state_dir_1 = require("./state-dir");
|
|
19
|
+
/** Attachments above this are left unread: a long recording or a huge image is
|
|
20
|
+
* a different conversation from a spoken line or a screenshot, and the
|
|
21
|
+
* transfer is not free. */
|
|
22
|
+
exports.INBOUND_MEDIA_MAX_BYTES = 25 * 1024 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* Locates the agent directory that image understanding needs.
|
|
25
|
+
*
|
|
26
|
+
* Image models are called with the agent's own credentials, so the pipeline
|
|
27
|
+
* refuses to run without this path — audio does not need it, which is why
|
|
28
|
+
* voice notes worked before images did. The platform exposes no resolver to
|
|
29
|
+
* plugins, so the documented layout is reconstructed here and checked before
|
|
30
|
+
* use: a wrong guess would fail the read anyway, and returning undefined lets
|
|
31
|
+
* the caller degrade instead of throwing.
|
|
32
|
+
*/
|
|
33
|
+
function resolveAgentDirForMedia(cfg) {
|
|
34
|
+
const stateDir = (0, state_dir_1.resolveStateDir)();
|
|
35
|
+
const configuredId = cfg?.agents?.defaults?.id;
|
|
36
|
+
const agentId = typeof configuredId === "string" && configuredId.trim() ? configuredId.trim() : "main";
|
|
37
|
+
const dir = node_path_1.default.join(stateDir, "agents", agentId, "agent");
|
|
38
|
+
return (0, node_fs_1.existsSync)(dir) ? dir : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Turns a downloaded attachment into text.
|
|
42
|
+
*
|
|
43
|
+
* Shared by the inbound path and by `fetch-media`: the backend choice lives in
|
|
44
|
+
* `runtime.mediaUnderstanding`, and both callers have to make exactly the same
|
|
45
|
+
* call — an image read on arrival and the same image read on request must not
|
|
46
|
+
* become two different readings because two call sites drifted.
|
|
47
|
+
*/
|
|
48
|
+
async function understandAttachmentFile(params) {
|
|
49
|
+
const media = params.runtime?.mediaUnderstanding;
|
|
50
|
+
if (!media)
|
|
51
|
+
return undefined;
|
|
52
|
+
const result = params.understanding === "transcript"
|
|
53
|
+
? await media.transcribeAudioFile({
|
|
54
|
+
filePath: params.filePath,
|
|
55
|
+
cfg: params.cfg,
|
|
56
|
+
mime: params.mimeType,
|
|
57
|
+
})
|
|
58
|
+
: await media.describeImageFile({
|
|
59
|
+
filePath: params.filePath,
|
|
60
|
+
cfg: params.cfg,
|
|
61
|
+
mime: params.mimeType,
|
|
62
|
+
agentDir: resolveAgentDirForMedia(params.cfg),
|
|
63
|
+
});
|
|
64
|
+
const text = typeof result?.text === "string" ? result.text.trim() : "";
|
|
65
|
+
return text || undefined;
|
|
66
|
+
}
|
|
67
|
+
async function readInboundAttachment(params) {
|
|
68
|
+
const media = params.runtime?.mediaUnderstanding;
|
|
69
|
+
const message = params.event?.message;
|
|
70
|
+
if (!media || !message) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
let downloaded;
|
|
74
|
+
try {
|
|
75
|
+
downloaded = await (0, media_1.downloadInboundMediaToTempFile)({
|
|
76
|
+
client: params.gram.getClient(),
|
|
77
|
+
message,
|
|
78
|
+
maxBytes: exports.INBOUND_MEDIA_MAX_BYTES,
|
|
79
|
+
tmpDir: node_os_1.default.tmpdir(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
params.log?.info?.("clawgram attachment download failed", {
|
|
84
|
+
accountId: params.accountId,
|
|
85
|
+
chatId: params.chatId,
|
|
86
|
+
messageId: params.messageId,
|
|
87
|
+
error: String(err),
|
|
88
|
+
});
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (!downloaded) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const read = await understandAttachmentFile({
|
|
96
|
+
runtime: params.runtime,
|
|
97
|
+
cfg: params.cfg,
|
|
98
|
+
filePath: downloaded.path,
|
|
99
|
+
mimeType: downloaded.mimeType,
|
|
100
|
+
understanding: downloaded.understanding,
|
|
101
|
+
});
|
|
102
|
+
if (!read) {
|
|
103
|
+
params.log?.info?.("clawgram attachment read empty", {
|
|
104
|
+
accountId: params.accountId,
|
|
105
|
+
chatId: params.chatId,
|
|
106
|
+
messageId: params.messageId,
|
|
107
|
+
understanding: downloaded.understanding,
|
|
108
|
+
});
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
params.log?.info?.("clawgram attachment read", {
|
|
112
|
+
accountId: params.accountId,
|
|
113
|
+
chatId: params.chatId,
|
|
114
|
+
messageId: params.messageId,
|
|
115
|
+
understanding: downloaded.understanding,
|
|
116
|
+
characters: read.length,
|
|
117
|
+
});
|
|
118
|
+
return { text: read, understanding: downloaded.understanding };
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
params.log?.info?.("clawgram attachment read failed", {
|
|
122
|
+
accountId: params.accountId,
|
|
123
|
+
chatId: params.chatId,
|
|
124
|
+
messageId: params.messageId,
|
|
125
|
+
understanding: downloaded.understanding,
|
|
126
|
+
error: String(err),
|
|
127
|
+
});
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
void (async () => {
|
|
132
|
+
try {
|
|
133
|
+
const { rm } = await import("node:fs/promises");
|
|
134
|
+
const { dirname } = await import("node:path");
|
|
135
|
+
await rm(dirname(downloaded.path), { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Leaving a temp file behind is not worth failing a delivered message.
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
}
|
|
142
|
+
}
|