openclaw-threema 0.5.2 → 0.6.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/README.md +31 -12
- package/index.ts +292 -16
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openclaw-threema
|
|
2
2
|
|
|
3
3
|
Threema Gateway channel plugin for [OpenClaw](https://github.com/openclaw/openclaw) — privacy-focused E2E encrypted messaging via the [Threema Gateway API](https://gateway.threema.ch/).
|
|
4
4
|
|
|
@@ -8,24 +8,25 @@ Threema Gateway channel plugin for [OpenClaw](https://github.com/openclaw/opencl
|
|
|
8
8
|
- **Media send/receive** — images, files, audio (E2E encrypted blobs)
|
|
9
9
|
- **Voice transcription** — automatic speech-to-text via local Whisper
|
|
10
10
|
- **Instant wake** — webhook-based message delivery (no polling)
|
|
11
|
+
- **Slash commands** — `/status`, `/compact`, etc. work natively via Threema (v0.6.0+)
|
|
11
12
|
- **CLI tools** — `openclaw threema send|send-file|status|keygen`
|
|
12
13
|
|
|
13
14
|
## Requirements
|
|
14
15
|
|
|
15
16
|
- A [Threema Gateway](https://gateway.threema.ch/) account (E2E mode)
|
|
16
|
-
- OpenClaw
|
|
17
|
+
- OpenClaw 2026.3.2+ with channel plugin support
|
|
17
18
|
- For voice transcription: [OpenAI Whisper](https://github.com/openai/whisper) installed locally
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
At the time of writing these lines you have to pay 1.600 "Credits" to get an ID.
|
|
21
|
-
Every Message costs another Credit (roughly EUR 0,02).
|
|
22
|
-
2.500 Credits are about EUR 55,00
|
|
20
|
+
> **Note:** Threema Gateway usage is not free. At the time of writing, you need ~1,600 credits to register a Gateway ID, and each message costs 1 credit (~€0.02). 2,500 credits cost approximately €55.
|
|
23
21
|
|
|
24
22
|
## Installation
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
|
-
# From npm
|
|
28
|
-
npm install
|
|
25
|
+
# From npm
|
|
26
|
+
npm install openclaw-threema
|
|
27
|
+
|
|
28
|
+
# From ClawHub
|
|
29
|
+
openclaw plugins install clawhub:threema
|
|
29
30
|
|
|
30
31
|
# Or as a local extension
|
|
31
32
|
cp -r . ~/.openclaw/extensions/threema/
|
|
@@ -77,7 +78,7 @@ https://your-host:18789/threema/webhook
|
|
|
77
78
|
|
|
78
79
|
The default port is `18789` (OpenClaw Gateway). The path matches `webhookPath` in your config (default: `/threema/webhook`).
|
|
79
80
|
|
|
80
|
-
**Note:** If you're behind a reverse proxy, adjust the URL accordingly. The plugin registers the endpoint at the configured `webhookPath`.
|
|
81
|
+
**Note:** If you're behind a reverse proxy (Cloudflare Tunnel, nginx, etc.), adjust the URL accordingly. The plugin registers the endpoint at the configured `webhookPath`.
|
|
81
82
|
|
|
82
83
|
### 3. Restart OpenClaw
|
|
83
84
|
|
|
@@ -96,7 +97,7 @@ openclaw threema send ABCD1234 "Hello from OpenClaw!"
|
|
|
96
97
|
|
|
97
98
|
| Policy | Description |
|
|
98
99
|
|--------|-------------|
|
|
99
|
-
| `allowlist` | Only IDs in `allowFrom` array (default) |
|
|
100
|
+
| `allowlist` | Only IDs in `allowFrom` array (default, recommended) |
|
|
100
101
|
| `open` | Accept from anyone |
|
|
101
102
|
| `disabled` | Reject all DMs |
|
|
102
103
|
|
|
@@ -104,7 +105,7 @@ openclaw threema send ABCD1234 "Hello from OpenClaw!"
|
|
|
104
105
|
|
|
105
106
|
When a voice message is received, the plugin automatically transcribes it using local Whisper (no API key needed). The transcription is included in the message delivered to the agent.
|
|
106
107
|
|
|
107
|
-
Whisper must be installed and accessible in PATH (e.g.,
|
|
108
|
+
Whisper must be installed and accessible in PATH (e.g., `pip install openai-whisper`).
|
|
108
109
|
|
|
109
110
|
## Message Types Supported
|
|
110
111
|
|
|
@@ -119,8 +120,26 @@ Whisper must be installed and accessible in PATH (e.g., via `pip install openai-
|
|
|
119
120
|
- Private keys never leave the host
|
|
120
121
|
- Webhook verification via HMAC-SHA256 (mandatory, verified before decryption)
|
|
121
122
|
- SSRF protection: redirect blocking, DNS rebinding checks, private IP filtering
|
|
123
|
+
- Secrets are redacted in all log output
|
|
124
|
+
|
|
125
|
+
## Changelog
|
|
126
|
+
|
|
127
|
+
### v0.6.0 (2026-04-17)
|
|
128
|
+
- **Channel Inbound Pipeline** — messages now go through OpenClaw's native channel pipeline instead of raw `enqueueSystemEvent`. This enables slash commands (`/status`, `/compact`, etc.) directly from Threema.
|
|
129
|
+
- **Graceful fallback** — automatically falls back to `enqueueSystemEvent` if the channel pipeline is unavailable (e.g., older OpenClaw versions).
|
|
130
|
+
- **Long message chunking** — replies exceeding 3,500 chars are split at newline boundaries.
|
|
131
|
+
|
|
132
|
+
### v0.5.2 (2026-03-30)
|
|
133
|
+
- OpenClaw 2026.3.2 compatibility fixes
|
|
134
|
+
- Health monitor improvements
|
|
135
|
+
- ClawHub publishing support (`compat.pluginApi`, `build.openclawVersion`)
|
|
136
|
+
|
|
137
|
+
### v0.4.5 (2026-02-17)
|
|
138
|
+
- Initial npm release
|
|
139
|
+
- Full E2E text + media support
|
|
140
|
+
- Voice transcription via Whisper
|
|
141
|
+
- Security hardening (SSRF protection, path restrictions, PII log reduction)
|
|
122
142
|
|
|
123
143
|
## License
|
|
124
144
|
|
|
125
145
|
MIT
|
|
126
|
-
|
package/index.ts
CHANGED
|
@@ -147,6 +147,163 @@ const MEDIA_INBOUND_DIR = path.join(
|
|
|
147
147
|
"inbound"
|
|
148
148
|
);
|
|
149
149
|
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Memory Briefing Hook
|
|
152
|
+
// ============================================================================
|
|
153
|
+
//
|
|
154
|
+
// Injects an up-to-date "acute state" snapshot from the workspace memory into
|
|
155
|
+
// every inbound Threema message that goes to the agent. This compensates for
|
|
156
|
+
// long-running sessions where MEMORY.md was loaded weeks ago and may not be
|
|
157
|
+
// salient for the current reply. Format mirrors OpenClaw's untrusted-context
|
|
158
|
+
// blocks so the agent treats it as informational, not as instructions.
|
|
159
|
+
//
|
|
160
|
+
// Sources read on each inbound (best-effort, fail-silent):
|
|
161
|
+
// 1. <workspace>/MEMORY.md -> only the leading "\ud83d\udccc Current State" block
|
|
162
|
+
// 2. <workspace>/memory/pending-actions.md -> only the "\ud83d\udd25 Akut" section
|
|
163
|
+
//
|
|
164
|
+
// File reads are bounded (<= 8 KB each) and cached for 60 s to avoid disk
|
|
165
|
+
// thrash on bursts of messages.
|
|
166
|
+
|
|
167
|
+
interface BriefingCacheEntry {
|
|
168
|
+
text: string;
|
|
169
|
+
expiresAt: number;
|
|
170
|
+
}
|
|
171
|
+
const briefingCache: Map<string, BriefingCacheEntry> = new Map();
|
|
172
|
+
const BRIEFING_CACHE_TTL_MS = 60_000;
|
|
173
|
+
const BRIEFING_MAX_BYTES = 8192;
|
|
174
|
+
|
|
175
|
+
function readBoundedFile(filePath: string): string {
|
|
176
|
+
try {
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
if (!stat.isFile()) return "";
|
|
179
|
+
const fd = fs.openSync(filePath, "r");
|
|
180
|
+
try {
|
|
181
|
+
const len = Math.min(stat.size, BRIEFING_MAX_BYTES);
|
|
182
|
+
const buf = Buffer.alloc(len);
|
|
183
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
184
|
+
return buf.toString("utf8");
|
|
185
|
+
} finally {
|
|
186
|
+
fs.closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function extractCurrentStateBlock(memoryMd: string): string {
|
|
194
|
+
if (!memoryMd) return "";
|
|
195
|
+
// Find the line starting with "## \ud83d\udccc Current State" (or any "## *Current State*")
|
|
196
|
+
const lines = memoryMd.split(/\r?\n/);
|
|
197
|
+
let startIdx = -1;
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
if (/^##\s.*Current State/i.test(lines[i])) {
|
|
200
|
+
startIdx = i;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (startIdx < 0) return "";
|
|
205
|
+
// Read until the next "## " header
|
|
206
|
+
const out: string[] = [];
|
|
207
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
208
|
+
if (i > startIdx && /^##\s/.test(lines[i])) break;
|
|
209
|
+
out.push(lines[i]);
|
|
210
|
+
}
|
|
211
|
+
return out.join("\n").trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function extractAcutePending(pendingMd: string): string {
|
|
215
|
+
if (!pendingMd) return "";
|
|
216
|
+
const lines = pendingMd.split(/\r?\n/);
|
|
217
|
+
let startIdx = -1;
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (/^##\s.*Akut/i.test(lines[i])) {
|
|
220
|
+
startIdx = i;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (startIdx < 0) return "";
|
|
225
|
+
const out: string[] = [];
|
|
226
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
227
|
+
if (i > startIdx && /^##\s/.test(lines[i])) break;
|
|
228
|
+
out.push(lines[i]);
|
|
229
|
+
}
|
|
230
|
+
return out.join("\n").trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveWorkspaceDir(cfg: OpenClawConfig | undefined): string | null {
|
|
234
|
+
// Best-effort: dig through the agents.defaults.workspace path used in this
|
|
235
|
+
// setup. We accept any string under agents.*.workspace too.
|
|
236
|
+
const root: any = cfg as any;
|
|
237
|
+
const candidates: any[] = [];
|
|
238
|
+
try {
|
|
239
|
+
if (root?.agents) {
|
|
240
|
+
for (const k of Object.keys(root.agents)) {
|
|
241
|
+
const w = root.agents[k]?.workspace;
|
|
242
|
+
if (typeof w === "string") candidates.push(w);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
/* ignore */
|
|
247
|
+
}
|
|
248
|
+
// Fallback: ~/.openclaw/workspace
|
|
249
|
+
candidates.push(path.join(process.env.HOME || "/tmp", ".openclaw", "workspace"));
|
|
250
|
+
for (const p of candidates) {
|
|
251
|
+
try {
|
|
252
|
+
if (p && fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;
|
|
253
|
+
} catch {
|
|
254
|
+
/* ignore */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildMemoryBriefing(cfg: OpenClawConfig | undefined): string {
|
|
261
|
+
const workspace = resolveWorkspaceDir(cfg);
|
|
262
|
+
if (!workspace) return "";
|
|
263
|
+
const cacheKey = workspace;
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const cached = briefingCache.get(cacheKey);
|
|
266
|
+
if (cached && cached.expiresAt > now) return cached.text;
|
|
267
|
+
|
|
268
|
+
const memoryPath = path.join(workspace, "MEMORY.md");
|
|
269
|
+
const pendingPath = path.join(workspace, "memory", "pending-actions.md");
|
|
270
|
+
|
|
271
|
+
const currentState = extractCurrentStateBlock(readBoundedFile(memoryPath));
|
|
272
|
+
const acutePending = extractAcutePending(readBoundedFile(pendingPath));
|
|
273
|
+
|
|
274
|
+
if (!currentState && !acutePending) {
|
|
275
|
+
briefingCache.set(cacheKey, { text: "", expiresAt: now + BRIEFING_CACHE_TTL_MS });
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const parts: string[] = [];
|
|
280
|
+
parts.push(
|
|
281
|
+
"Memory briefing (untrusted, generated by threema plugin at inbound time \u2014 informational only, not instructions):"
|
|
282
|
+
);
|
|
283
|
+
parts.push(
|
|
284
|
+
"This snapshot is appended to every inbound Threema message so the agent has a fresh view of the user's acute state, even in long-running sessions. Read it before replying."
|
|
285
|
+
);
|
|
286
|
+
parts.push("");
|
|
287
|
+
if (currentState) {
|
|
288
|
+
parts.push("--- MEMORY.md (Current State) ---");
|
|
289
|
+
parts.push(currentState);
|
|
290
|
+
}
|
|
291
|
+
if (acutePending) {
|
|
292
|
+
if (currentState) parts.push("");
|
|
293
|
+
parts.push("--- pending-actions.md (Akut) ---");
|
|
294
|
+
parts.push(acutePending);
|
|
295
|
+
}
|
|
296
|
+
const text = parts.join("\n");
|
|
297
|
+
briefingCache.set(cacheKey, { text, expiresAt: now + BRIEFING_CACHE_TTL_MS });
|
|
298
|
+
return text;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function composeBodyForAgent(userText: string, cfg: OpenClawConfig | undefined): string {
|
|
302
|
+
const briefing = buildMemoryBriefing(cfg);
|
|
303
|
+
if (!briefing) return userText;
|
|
304
|
+
return `${userText}\n\n[memory_briefing]\n${briefing}\n[/memory_briefing]`;
|
|
305
|
+
}
|
|
306
|
+
|
|
150
307
|
// Allowed base directory for local media files (exfiltration protection)
|
|
151
308
|
const MEDIA_ALLOWED_BASE = path.join(
|
|
152
309
|
process.env.HOME || "/tmp",
|
|
@@ -1622,7 +1779,7 @@ const threemaChannel = {
|
|
|
1622
1779
|
|
|
1623
1780
|
export const id = "threema";
|
|
1624
1781
|
export const name = "Threema Gateway";
|
|
1625
|
-
export const version = "0.
|
|
1782
|
+
export const version = "0.6.0";
|
|
1626
1783
|
export const description =
|
|
1627
1784
|
"Threema messaging channel via Threema Gateway API (E2E encrypted, with media support)";
|
|
1628
1785
|
|
|
@@ -1782,25 +1939,144 @@ export default function register(api: any) {
|
|
|
1782
1939
|
api.logger?.info?.("Threema text message received");
|
|
1783
1940
|
api.logger?.debug?.(`Threema text from=${from} messageId=${messageId}`);
|
|
1784
1941
|
|
|
1785
|
-
// Dispatch
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1942
|
+
// Dispatch through the proper Channel Inbound Pipeline
|
|
1943
|
+
// This enables slash-command parsing (/status, /compact, etc.)
|
|
1944
|
+
const channelRuntime = runtime?.channel;
|
|
1945
|
+
if (channelRuntime?.routing?.resolveAgentRoute && channelRuntime?.reply?.finalizeInboundContext && channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
1946
|
+
try {
|
|
1947
|
+
const currentCfg = runtime.config.loadConfig();
|
|
1948
|
+
|
|
1949
|
+
// 1. Resolve the agent route and session key
|
|
1950
|
+
const route = channelRuntime.routing.resolveAgentRoute({
|
|
1951
|
+
cfg: currentCfg,
|
|
1792
1952
|
channel: "threema",
|
|
1793
|
-
to: from,
|
|
1794
|
-
from: from,
|
|
1795
1953
|
accountId: "default",
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent");
|
|
1954
|
+
peer: { kind: "direct", id: from },
|
|
1955
|
+
});
|
|
1799
1956
|
|
|
1800
|
-
|
|
1801
|
-
|
|
1957
|
+
const sessionKey = channelRuntime.routing.buildAgentSessionKey({
|
|
1958
|
+
agentId: route.agentId,
|
|
1959
|
+
channel: "threema",
|
|
1960
|
+
accountId: "default",
|
|
1961
|
+
peer: { kind: "direct", id: from },
|
|
1962
|
+
dmScope: "per-account-channel-peer",
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// 2. Check if sender is command-authorized (in allowFrom list)
|
|
1966
|
+
const senderAllowed = allowFrom.length === 0 || allowFrom.includes(from);
|
|
1967
|
+
|
|
1968
|
+
// 3. Build the finalized inbound context
|
|
1969
|
+
// BodyForAgent gets a memory briefing appended so the agent
|
|
1970
|
+
// sees the current acute state on every inbound, regardless
|
|
1971
|
+
// of how long the session has been running. CommandBody and
|
|
1972
|
+
// RawBody stay clean for slash-command parsing.
|
|
1973
|
+
const bodyForAgent = composeBodyForAgent(decrypted.text, currentCfg);
|
|
1974
|
+
const msgCtx = channelRuntime.reply.finalizeInboundContext({
|
|
1975
|
+
Body: decrypted.text,
|
|
1976
|
+
RawBody: decrypted.text,
|
|
1977
|
+
CommandBody: decrypted.text,
|
|
1978
|
+
BodyForAgent: bodyForAgent,
|
|
1979
|
+
From: `threema:${from}`,
|
|
1980
|
+
To: `threema:${ownGatewayId}`,
|
|
1981
|
+
SessionKey: sessionKey,
|
|
1982
|
+
AccountId: "default",
|
|
1983
|
+
OriginatingChannel: "threema",
|
|
1984
|
+
OriginatingTo: `threema:${from}`,
|
|
1985
|
+
ChatType: "direct" as const,
|
|
1986
|
+
SenderName: senderLabel,
|
|
1987
|
+
SenderId: from,
|
|
1988
|
+
Provider: "threema",
|
|
1989
|
+
Surface: "threema",
|
|
1990
|
+
ConversationLabel: senderLabel || from,
|
|
1991
|
+
Timestamp: Date.now(),
|
|
1992
|
+
CommandAuthorized: senderAllowed,
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
// 4. Dispatch through the full pipeline (command parsing + agent)
|
|
1996
|
+
const replyClient = new ThreemaClient(getThreemaConfig(currentCfg)!);
|
|
1997
|
+
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1998
|
+
ctx: msgCtx,
|
|
1999
|
+
cfg: currentCfg,
|
|
2000
|
+
dispatcherOptions: {
|
|
2001
|
+
deliver: async (payload: any) => {
|
|
2002
|
+
const text = payload.text ?? payload.body;
|
|
2003
|
+
if (!text) return;
|
|
2004
|
+
// Chunk long replies if needed
|
|
2005
|
+
const limit = getThreemaConfig(currentCfg)?.textChunkLimit ?? 3500;
|
|
2006
|
+
if (text.length <= limit) {
|
|
2007
|
+
if (replyClient.isE2EEnabled) {
|
|
2008
|
+
await replyClient.sendE2E(from, text);
|
|
2009
|
+
} else {
|
|
2010
|
+
await replyClient.sendSimple(from, text);
|
|
2011
|
+
}
|
|
2012
|
+
} else {
|
|
2013
|
+
// Split into chunks at newline boundaries
|
|
2014
|
+
const chunks: string[] = [];
|
|
2015
|
+
let remaining = text;
|
|
2016
|
+
while (remaining.length > 0) {
|
|
2017
|
+
if (remaining.length <= limit) {
|
|
2018
|
+
chunks.push(remaining);
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
let splitIdx = remaining.lastIndexOf("\n", limit);
|
|
2022
|
+
if (splitIdx <= 0) splitIdx = limit;
|
|
2023
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
2024
|
+
remaining = remaining.slice(splitIdx).replace(/^\n/, "");
|
|
2025
|
+
}
|
|
2026
|
+
for (const chunk of chunks) {
|
|
2027
|
+
if (replyClient.isE2EEnabled) {
|
|
2028
|
+
await replyClient.sendE2E(from, chunk);
|
|
2029
|
+
} else {
|
|
2030
|
+
await replyClient.sendSimple(from, chunk);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
onReplyStart: () => {
|
|
2036
|
+
api.logger?.info?.(`Threema: agent reply started for ${from}`);
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
api.logger?.info?.("Threema message dispatched via Channel Inbound Pipeline");
|
|
2041
|
+
} catch (pipelineErr: any) {
|
|
2042
|
+
api.logger?.error?.(`Threema inbound pipeline error: ${pipelineErr.message}`);
|
|
2043
|
+
// Fallback to enqueueSystemEvent if pipeline fails
|
|
2044
|
+
const enqueue = runtime?.system?.enqueueSystemEvent;
|
|
2045
|
+
if (enqueue) {
|
|
2046
|
+
const envelope = `[Threema message from ${senderLabel} (${from})]\n${decrypted.text}`;
|
|
2047
|
+
enqueue(envelope, {
|
|
2048
|
+
sessionKey: "agent:main:main",
|
|
2049
|
+
deliveryContext: {
|
|
2050
|
+
channel: "threema",
|
|
2051
|
+
to: from,
|
|
2052
|
+
from: from,
|
|
2053
|
+
accountId: "default",
|
|
2054
|
+
},
|
|
2055
|
+
});
|
|
2056
|
+
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent (fallback)");
|
|
2057
|
+
wakeAgent(config);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
1802
2060
|
} else {
|
|
1803
|
-
|
|
2061
|
+
// Fallback: use enqueueSystemEvent if channel runtime not available
|
|
2062
|
+
api.logger?.warn?.("Channel inbound pipeline not available, falling back to enqueueSystemEvent");
|
|
2063
|
+
const enqueue = runtime?.system?.enqueueSystemEvent;
|
|
2064
|
+
if (enqueue) {
|
|
2065
|
+
const envelope = `[Threema message from ${senderLabel} (${from})]\n${decrypted.text}`;
|
|
2066
|
+
enqueue(envelope, {
|
|
2067
|
+
sessionKey: "agent:main:main",
|
|
2068
|
+
deliveryContext: {
|
|
2069
|
+
channel: "threema",
|
|
2070
|
+
to: from,
|
|
2071
|
+
from: from,
|
|
2072
|
+
accountId: "default",
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent (legacy)");
|
|
2076
|
+
wakeAgent(config);
|
|
2077
|
+
} else {
|
|
2078
|
+
api.logger?.warn?.("Neither channel pipeline nor enqueueSystemEvent available");
|
|
2079
|
+
}
|
|
1804
2080
|
}
|
|
1805
2081
|
} else if (decrypted?.type === 0x17 && decrypted?.fileMessage) {
|
|
1806
2082
|
// File message - PII only on debug level
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-threema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Threema Gateway channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
"threema-gateway"
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
|
-
"id": "threema"
|
|
22
|
+
"id": "threema",
|
|
23
|
+
"compat": {
|
|
24
|
+
"pluginApi": ">=1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"build": {
|
|
27
|
+
"openclawVersion": "2026.3.28"
|
|
28
|
+
}
|
|
23
29
|
},
|
|
24
30
|
"dependencies": {
|
|
25
31
|
"tweetnacl": "^1.0.3",
|