r2-relay-channel 0.3.5

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/dist/channel.d.ts +16 -0
  4. package/dist/channel.js +271 -0
  5. package/dist/checkpoint-store.d.ts +4 -0
  6. package/dist/checkpoint-store.js +16 -0
  7. package/dist/commands.d.ts +2 -0
  8. package/dist/commands.js +54 -0
  9. package/dist/config.d.ts +76 -0
  10. package/dist/config.js +123 -0
  11. package/dist/conversation-targets.d.ts +32 -0
  12. package/dist/conversation-targets.js +120 -0
  13. package/dist/inbound.d.ts +38 -0
  14. package/dist/inbound.js +440 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +26 -0
  17. package/dist/lifecycle.d.ts +64 -0
  18. package/dist/lifecycle.js +296 -0
  19. package/dist/media.d.ts +30 -0
  20. package/dist/media.js +361 -0
  21. package/dist/message-adapter.d.ts +27 -0
  22. package/dist/message-adapter.js +53 -0
  23. package/dist/openclaw.plugin.json +41 -0
  24. package/dist/outbound.d.ts +28 -0
  25. package/dist/outbound.js +141 -0
  26. package/dist/protocol.d.ts +2 -0
  27. package/dist/protocol.js +1 -0
  28. package/dist/r2-relay.d.ts +1 -0
  29. package/dist/r2-relay.js +1 -0
  30. package/dist/r2client.d.ts +33 -0
  31. package/dist/r2client.js +137 -0
  32. package/dist/relay-core/checkpoint.d.ts +31 -0
  33. package/dist/relay-core/checkpoint.js +95 -0
  34. package/dist/relay-core/errors.d.ts +9 -0
  35. package/dist/relay-core/errors.js +18 -0
  36. package/dist/relay-core/index.d.ts +6 -0
  37. package/dist/relay-core/index.js +6 -0
  38. package/dist/relay-core/keyspace.d.ts +24 -0
  39. package/dist/relay-core/keyspace.js +60 -0
  40. package/dist/relay-core/object-store.d.ts +20 -0
  41. package/dist/relay-core/object-store.js +1 -0
  42. package/dist/relay-core/transport.d.ts +48 -0
  43. package/dist/relay-core/transport.js +326 -0
  44. package/dist/relay-core/types.d.ts +220 -0
  45. package/dist/relay-core/types.js +1 -0
  46. package/dist/runtime.d.ts +9 -0
  47. package/dist/runtime.js +54 -0
  48. package/dist/service.d.ts +51 -0
  49. package/dist/service.js +186 -0
  50. package/dist/service.test.d.ts +1 -0
  51. package/dist/service.test.js +27 -0
  52. package/dist/session-publication.d.ts +7 -0
  53. package/dist/session-publication.js +365 -0
  54. package/dist/setup-entry.d.ts +6 -0
  55. package/dist/setup-entry.js +49 -0
  56. package/dist/setup.d.ts +3 -0
  57. package/dist/setup.js +291 -0
  58. package/dist/target.d.ts +6 -0
  59. package/dist/target.js +53 -0
  60. package/dist/webhook.d.ts +18 -0
  61. package/dist/webhook.js +225 -0
  62. package/openclaw.plugin.json +41 -0
  63. package/package.json +62 -0
@@ -0,0 +1,440 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
4
+ import { buildChannelInboundMediaPayload, toInboundMediaFacts, } from "openclaw/plugin-sdk/channel-inbound";
5
+ import { getRelayRuntime } from "./runtime.js";
6
+ import { rememberConversationTarget } from "./conversation-targets.js";
7
+ import { emitRelayDebug, touchRuntime } from "./lifecycle.js";
8
+ import { sendRelayPayloadMessage } from "./outbound.js";
9
+ import { formatAttachmentContext, normalizeAttachmentContentType, resolveInboundAttachmentBestEffortMaxBytes, resolveInboundAttachmentMaxBytes, resolveRelayPayloadMediaUrls, } from "./media.js";
10
+ import { agentIdFromSessionKey, readPublishedConversationEntry, schedulePublishedIdentityRefresh, } from "./session-publication.js";
11
+ export async function dispatchInboundMessage(params) {
12
+ const core = getRelayRuntime();
13
+ const resolvedRoute = core.channel.routing.resolveAgentRoute({
14
+ cfg: params.cfg,
15
+ channel: "r2-relay-channel",
16
+ accountId: params.account.accountId,
17
+ peer: {
18
+ kind: "direct",
19
+ id: params.senderId,
20
+ },
21
+ });
22
+ const conversationId = (params.route.conversation_id ?? "").trim();
23
+ const route = conversationId
24
+ ? {
25
+ ...resolvedRoute,
26
+ agentId: params.route.agent_id?.trim() || agentIdFromSessionKey(conversationId),
27
+ sessionKey: conversationId,
28
+ }
29
+ : resolvedRoute;
30
+ const storePath = core.channel.session.resolveStorePath(params.cfg.session?.store, {
31
+ agentId: route.agentId,
32
+ });
33
+ const attachmentContext = formatAttachmentContext(params.attachments);
34
+ const agentText = attachmentContext
35
+ ? `${params.text}\n\n${attachmentContext}`
36
+ : params.text;
37
+ const inboundMedia = [];
38
+ if (params.attachments) {
39
+ const logger = getRelayRuntime().logging.getChildLogger();
40
+ for (const att of params.attachments) {
41
+ if (!att.key) {
42
+ continue;
43
+ }
44
+ const maxBytes = resolveInboundAttachmentMaxBytes(att);
45
+ const bestEffortMaxBytes = resolveInboundAttachmentBestEffortMaxBytes(att, maxBytes);
46
+ try {
47
+ logger.info(`[${params.account.accountId}] staging inbound relay attachment key=${att.key} name=${att.file_name ?? ""} type=${att.content_type ?? ""} kind=${att.kind ?? "unknown"} declaredSize=${att.size ?? -1} maxBytes=${maxBytes} bestEffortMaxBytes=${bestEffortMaxBytes}`);
48
+ logger.info(`[${params.account.accountId}] inbound relay attachment fetching object key=${att.key}`);
49
+ const fetchedObject = await params.service.getAttachmentObject(att.key);
50
+ if (!fetchedObject?.Body) {
51
+ throw new Error(`AttachmentNotFound: key=${att.key}`);
52
+ }
53
+ const chunks = [];
54
+ let total = 0;
55
+ let nextProgressBytes = 1024 * 1024;
56
+ for await (const chunk of fetchedObject.Body) {
57
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
58
+ chunks.push(buf);
59
+ total += buf.length;
60
+ if (total >= nextProgressBytes) {
61
+ logger.info(`[${params.account.accountId}] inbound relay attachment streaming progress key=${att.key} downloadedBytes=${total} declaredSize=${att.size ?? -1}`);
62
+ nextProgressBytes += 1024 * 1024;
63
+ }
64
+ if (total > bestEffortMaxBytes) {
65
+ throw new Error(`AttachmentTooLarge: streamedSize>${bestEffortMaxBytes}`);
66
+ }
67
+ }
68
+ const buffer = Buffer.concat(chunks);
69
+ logger.info(`[${params.account.accountId}] inbound relay attachment fetched object key=${att.key} bytes=${buffer.length}`);
70
+ const contentType = normalizeAttachmentContentType(fetchedObject.ContentType, att.content_type, att.file_name);
71
+ const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound", bestEffortMaxBytes, att.file_name ?? undefined);
72
+ const workspaceMediaPath = stageInboundMediaForAgentWorkspace({
73
+ workspaceDir: resolveAgentWorkspaceDirFromConfig(params.cfg, route.agentId),
74
+ sourcePath: saved.path,
75
+ originalFileName: att.file_name,
76
+ buffer,
77
+ });
78
+ inboundMedia.push({
79
+ path: workspaceMediaPath,
80
+ contentType: saved.contentType ?? contentType ?? att.content_type ?? "application/octet-stream",
81
+ kind: att.kind === "file" ? "document" : att.kind,
82
+ messageId: params.messageId,
83
+ });
84
+ logger.info(`[${params.account.accountId}] staged inbound relay attachment key=${att.key} savedPath=${saved.path} workspacePath=${workspaceMediaPath} savedType=${saved.contentType ?? contentType ?? att.content_type ?? "application/octet-stream"} bytes=${buffer.length}`);
85
+ }
86
+ catch (err) {
87
+ getRelayRuntime().logging.getChildLogger().error(`[${params.account.accountId}] failed to stage inbound relay attachment ${att.key}: ${String(err)}`);
88
+ }
89
+ }
90
+ }
91
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
92
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
93
+ storePath,
94
+ sessionKey: route.sessionKey,
95
+ });
96
+ const body = core.channel.reply.formatAgentEnvelope({
97
+ channel: "R2 Relay",
98
+ from: params.senderId,
99
+ timestamp: params.timestamp,
100
+ previousTimestamp,
101
+ envelope: envelopeOptions,
102
+ body: agentText,
103
+ });
104
+ const mediaPayload = buildChannelInboundMediaPayload(toInboundMediaFacts(inboundMedia, { messageId: params.messageId }));
105
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
106
+ Body: body,
107
+ BodyForAgent: agentText,
108
+ RawBody: params.text,
109
+ CommandBody: params.text,
110
+ CommandAuthorized: true,
111
+ From: `r2-relay-channel:${params.senderId}`,
112
+ To: `r2-relay-channel:${params.account.serverId}`,
113
+ SessionKey: route.sessionKey,
114
+ AccountId: route.accountId,
115
+ ChatType: "direct",
116
+ ConversationLabel: params.senderId,
117
+ SenderId: params.senderId,
118
+ Provider: "r2-relay-channel",
119
+ Surface: "r2-relay-channel",
120
+ MessageSid: params.messageId,
121
+ Timestamp: params.timestamp,
122
+ OriginatingChannel: "r2-relay-channel",
123
+ OriginatingTo: `r2-relay-channel:${params.account.serverId}`,
124
+ ...mediaPayload,
125
+ });
126
+ rememberConversationTarget({
127
+ channel: "r2-relay-channel",
128
+ accountId: params.account.accountId,
129
+ conversationId: ctxPayload.From,
130
+ threadId: null,
131
+ peer: params.senderId,
132
+ relayConversationId: route.sessionKey,
133
+ });
134
+ const conversationEntry = await readPublishedConversationEntry(route.sessionKey);
135
+ const outboundMeta = {
136
+ route: conversationEntry?.route ?? {
137
+ agent_id: route.agentId,
138
+ conversation_id: route.sessionKey,
139
+ instance_id: null,
140
+ },
141
+ };
142
+ const createStreamId = () => `stream-${params.messageId}-${Date.now()}`;
143
+ const streamState = {
144
+ streamId: createStreamId(),
145
+ seq: 0,
146
+ lastText: "",
147
+ lastEmitAt: 0,
148
+ active: false,
149
+ };
150
+ const partialIntervalMs = 2000;
151
+ const partialMinGrowthChars = 120;
152
+ const resetStream = () => {
153
+ streamState.streamId = createStreamId();
154
+ streamState.seq = 0;
155
+ streamState.lastText = "";
156
+ streamState.lastEmitAt = 0;
157
+ streamState.active = false;
158
+ };
159
+ const normalizeStreamText = (text) => text.replace(/\r\n/g, "\n");
160
+ const emitPartialSnapshot = async (text) => {
161
+ const normalized = normalizeStreamText(text);
162
+ if (!normalized.trim()) {
163
+ return;
164
+ }
165
+ if (streamState.active && normalized === streamState.lastText) {
166
+ return;
167
+ }
168
+ if (streamState.active && streamState.lastText && !normalized.startsWith(streamState.lastText)) {
169
+ return;
170
+ }
171
+ if (!streamState.active) {
172
+ streamState.streamId = createStreamId();
173
+ streamState.seq = 0;
174
+ streamState.lastText = "";
175
+ streamState.lastEmitAt = 0;
176
+ streamState.active = true;
177
+ }
178
+ streamState.seq += 1;
179
+ streamState.lastText = normalized;
180
+ streamState.lastEmitAt = Date.now();
181
+ try {
182
+ await params.service.sendMessage(params.senderId, {
183
+ route: outboundMeta.route,
184
+ content: { type: "text", text: normalized },
185
+ delivery: {
186
+ stream: {
187
+ stream_id: streamState.streamId,
188
+ seq: streamState.seq,
189
+ state: "partial",
190
+ },
191
+ },
192
+ });
193
+ emitRelayDebug(params.log, `[${params.account.accountId}] sent relay partial reply snapshot`, {
194
+ senderId: params.senderId,
195
+ route: outboundMeta.route,
196
+ streamId: streamState.streamId,
197
+ streamSeq: streamState.seq,
198
+ textLength: normalized.length,
199
+ });
200
+ touchRuntime(params.account.accountId, {
201
+ lastOutboundAt: Date.now(),
202
+ lastError: null,
203
+ });
204
+ }
205
+ catch (err) {
206
+ const message = err instanceof Error ? err.message : String(err);
207
+ params.log?.error?.(`[${params.account.accountId}] failed to send relay partial reply snapshot: ${message}`, {
208
+ senderId: params.senderId,
209
+ route: outboundMeta.route,
210
+ streamId: streamState.streamId,
211
+ streamSeq: streamState.seq,
212
+ textLength: normalized.length,
213
+ });
214
+ throw err;
215
+ }
216
+ };
217
+ await core.channel.session.recordInboundSession({
218
+ storePath,
219
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
220
+ ctx: ctxPayload,
221
+ onRecordError: () => { },
222
+ });
223
+ schedulePublishedIdentityRefresh(params.service, params.account);
224
+ if (params.messageId && params.senderId) {
225
+ try {
226
+ await sendProcessedConfirmation({
227
+ account: params.account,
228
+ service: params.service,
229
+ targetPeer: params.senderId,
230
+ targetMessageId: params.messageId,
231
+ route: outboundMeta.route,
232
+ log: params.log,
233
+ });
234
+ }
235
+ catch (confirmErr) {
236
+ params.log?.warn?.(`[${params.account.accountId}] failed to send processed confirmation: ${confirmErr instanceof Error ? confirmErr.message : String(confirmErr)}`, {
237
+ senderId: params.senderId,
238
+ messageId: params.messageId,
239
+ route: outboundMeta.route,
240
+ });
241
+ }
242
+ }
243
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
244
+ cfg: params.cfg,
245
+ agentId: route.agentId,
246
+ channel: "r2-relay-channel",
247
+ accountId: params.account.accountId,
248
+ });
249
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
250
+ ctx: ctxPayload,
251
+ cfg: params.cfg,
252
+ dispatcherOptions: {
253
+ ...replyPipeline,
254
+ deliver: async (payload) => {
255
+ const text = normalizeStreamText(payload.text ?? "");
256
+ const mediaUrls = resolveRelayPayloadMediaUrls(payload);
257
+ emitRelayDebug(params.log, `[${params.account.accountId}] relay final payload received`, {
258
+ senderId: params.senderId,
259
+ route: outboundMeta.route,
260
+ hasText: Boolean(text.trim()),
261
+ mediaUrl: payload.mediaUrl ?? null,
262
+ mediaUrls,
263
+ rawMediaUrls: payload.mediaUrls ?? null,
264
+ textHasMediaDirective: text.includes("MEDIA:"),
265
+ });
266
+ if (streamState.active && text.trim().length > 0) {
267
+ if (streamState.lastText && !text.startsWith(streamState.lastText)) {
268
+ return;
269
+ }
270
+ streamState.lastText = text;
271
+ streamState.lastEmitAt = Date.now();
272
+ }
273
+ try {
274
+ await sendRelayPayloadMessage({
275
+ cfg: params.cfg,
276
+ to: params.senderId,
277
+ payload: {
278
+ ...payload,
279
+ text,
280
+ mediaUrls,
281
+ mediaUrl: mediaUrls[0],
282
+ channelData: payload.channelData,
283
+ },
284
+ accountId: params.account.accountId,
285
+ log: params.log,
286
+ source: "replyPipeline.final",
287
+ meta: {
288
+ route: outboundMeta.route,
289
+ workspaceDir: resolveAgentWorkspaceDirFromConfig(params.cfg, route.agentId),
290
+ },
291
+ });
292
+ emitRelayDebug(params.log, `[${params.account.accountId}] sent relay final reply`, {
293
+ senderId: params.senderId,
294
+ route: outboundMeta.route,
295
+ textLength: text.length,
296
+ hasChannelData: Boolean(payload.channelData && Object.keys(payload.channelData).length > 0),
297
+ mediaCount: mediaUrls.length,
298
+ });
299
+ resetStream();
300
+ }
301
+ catch (err) {
302
+ const message = err instanceof Error ? err.message : String(err);
303
+ params.log?.error?.(`[${params.account.accountId}] failed to send relay final reply: ${message}`, {
304
+ senderId: params.senderId,
305
+ route: outboundMeta.route,
306
+ textLength: text.length,
307
+ hasChannelData: Boolean(payload.channelData && Object.keys(payload.channelData).length > 0),
308
+ mediaCount: mediaUrls.length,
309
+ });
310
+ throw err;
311
+ }
312
+ },
313
+ onError: (err) => {
314
+ throw err instanceof Error ? err : new Error(String(err));
315
+ },
316
+ },
317
+ replyOptions: {
318
+ onModelSelected,
319
+ onAssistantMessageStart: async () => { },
320
+ onReasoningEnd: async () => { },
321
+ onPartialReply: async (payload) => {
322
+ const text = payload.text ?? "";
323
+ const normalized = normalizeStreamText(text);
324
+ if (!normalized.trim()) {
325
+ return;
326
+ }
327
+ if (streamState.active && normalized === streamState.lastText) {
328
+ return;
329
+ }
330
+ const now = Date.now();
331
+ const growth = normalized.length - streamState.lastText.length;
332
+ const enoughTimePassed = now - streamState.lastEmitAt >= partialIntervalMs;
333
+ const enoughGrowth = growth >= partialMinGrowthChars;
334
+ if (!enoughTimePassed && !enoughGrowth) {
335
+ return;
336
+ }
337
+ await emitPartialSnapshot(normalized);
338
+ },
339
+ },
340
+ });
341
+ }
342
+ export function formatInboundRelayBody(msg) {
343
+ const content = msg.content ?? {};
344
+ const contentType = content.type ?? "text";
345
+ if (contentType === "reaction") {
346
+ const emoji = content.emoji ?? "";
347
+ const target = content.target_msg_id ?? "unknown";
348
+ return content.remove
349
+ ? `Reaction removed: ${emoji || "(cleared)"} on msg ${target}`
350
+ : `Reaction added: ${emoji || "(empty)"} on msg ${target}`;
351
+ }
352
+ if (contentType === "approval_response") {
353
+ const approvalId = content.approval_id?.trim();
354
+ const decision = content.decision?.trim();
355
+ return approvalId && decision ? `/approve ${approvalId} ${decision}` : "";
356
+ }
357
+ const body = (contentType === "text" ? content.text : null)?.trim() ?? "";
358
+ if (body)
359
+ return body;
360
+ const atts = contentType === "text" ? content.attachments : null;
361
+ if (atts && atts.length > 0) {
362
+ if (atts.length === 1) {
363
+ const att = atts[0];
364
+ const label = att.file_name ?? (att.kind && att.kind !== "unknown" ? att.kind : "attachment");
365
+ return `[Sent ${label}]`;
366
+ }
367
+ return `[Sent ${atts.length} attachments]`;
368
+ }
369
+ return body;
370
+ }
371
+ export function classifyInboundRelayMessageFailure(err) {
372
+ const message = err instanceof Error ? err.message : String(err);
373
+ if (message.includes("AttachmentTooLarge")) {
374
+ return "error";
375
+ }
376
+ if (message.includes("AttachmentNotFound")) {
377
+ return "error";
378
+ }
379
+ return "error";
380
+ }
381
+ export function resolvePublishedServerLimits(limits) {
382
+ return {
383
+ inbound_attachment_max_bytes: {
384
+ image: limits.maxImageBytes,
385
+ video: limits.maxVideoBytes,
386
+ audio: limits.maxAudioBytes,
387
+ file: limits.maxDocumentBytes,
388
+ },
389
+ oversize_attachment_behavior: "reject",
390
+ };
391
+ }
392
+ async function sendProcessedConfirmation(params) {
393
+ emitRelayDebug(params.log, `[${params.account.accountId}] sending processed confirmation`, {
394
+ targetPeer: params.targetPeer,
395
+ targetMessageId: params.targetMessageId,
396
+ route: params.route,
397
+ });
398
+ const result = await params.service.sendMessage(params.targetPeer, {
399
+ route: params.route,
400
+ content: {
401
+ type: "reaction",
402
+ target_msg_id: params.targetMessageId,
403
+ emoji: "✅",
404
+ remove: false,
405
+ },
406
+ });
407
+ emitRelayDebug(params.log, `[${params.account.accountId}] processed confirmation sent`, {
408
+ targetPeer: params.targetPeer,
409
+ targetMessageId: params.targetMessageId,
410
+ confirmationKey: result.key,
411
+ confirmationMessageId: result.messageId,
412
+ route: params.route,
413
+ });
414
+ }
415
+ function resolveAgentWorkspaceDirFromConfig(cfg, agentId) {
416
+ const agents = (cfg.agents ?? {});
417
+ const normalizedAgentId = agentId?.trim() || "main";
418
+ const matched = agents.list?.find((entry) => entry?.id === normalizedAgentId)?.workspace?.trim();
419
+ if (matched) {
420
+ return path.resolve(matched);
421
+ }
422
+ const fallback = agents.defaults?.workspace?.trim();
423
+ if (fallback) {
424
+ return path.resolve(fallback);
425
+ }
426
+ return path.join(process.env.HOME ?? process.cwd(), ".openclaw", `workspace${normalizedAgentId === "main" ? "" : `-${normalizedAgentId}`}`);
427
+ }
428
+ function stageInboundMediaForAgentWorkspace(params) {
429
+ const sourceBase = path.basename(params.sourcePath);
430
+ const originalBase = params.originalFileName ? sanitizeWorkspaceMediaFileName(params.originalFileName) : "";
431
+ const fileName = sourceBase || originalBase || `relay-inbound-${Date.now()}`;
432
+ const mediaDir = path.join(params.workspaceDir, "media", "inbound");
433
+ const workspacePath = path.join(mediaDir, fileName);
434
+ fs.mkdirSync(mediaDir, { recursive: true });
435
+ fs.writeFileSync(workspacePath, params.buffer, { mode: 0o600 });
436
+ return workspacePath;
437
+ }
438
+ function sanitizeWorkspaceMediaFileName(value) {
439
+ return path.basename(value).replace(/[^\p{L}\p{N}._-]+/gu, "_").replace(/^_+|_+$/g, "").slice(0, 100);
440
+ }
@@ -0,0 +1,4 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { r2RelayPlugin } from "./channel.js";
3
+ declare const r2RelayEntry: ReturnType<typeof defineChannelPluginEntry<typeof r2RelayPlugin>>;
4
+ export default r2RelayEntry;
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { r2RelayPlugin } from "./channel.js";
3
+ import { setRelayConfig, setRelayRuntime, setRelayStateDir } from "./runtime.js";
4
+ import { registerRelayWebhookRoute } from "./webhook.js";
5
+ import { registerRelayCommands } from "./commands.js";
6
+ const r2RelayEntry = defineChannelPluginEntry({
7
+ id: "r2-relay-channel",
8
+ name: "R2 Relay",
9
+ description: "Cloudflare R2 relay channel plugin for OpenClaw",
10
+ plugin: r2RelayPlugin,
11
+ setRuntime(runtime) {
12
+ setRelayRuntime(runtime);
13
+ },
14
+ registerFull(api) {
15
+ setRelayConfig(api.config);
16
+ registerRelayWebhookRoute(api);
17
+ registerRelayCommands(api);
18
+ api.registerService({
19
+ id: "r2-relay-channel-state",
20
+ start(ctx) {
21
+ setRelayStateDir(ctx.stateDir);
22
+ },
23
+ });
24
+ },
25
+ });
26
+ export default r2RelayEntry;
@@ -0,0 +1,64 @@
1
+ import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
2
+ import { type RelayCheckpointState } from "./checkpoint-store.js";
3
+ import type { ResolvedR2RelayAccount } from "./config.js";
4
+ import type { AttachmentRef, RelayMessage } from "./protocol.js";
5
+ import { Service } from "./service.js";
6
+ export type RelayRuntimeState = Omit<ReturnType<typeof createDefaultChannelRuntimeState>, "running" | "lastStartAt" | "lastStopAt" | "lastError"> & {
7
+ running: boolean;
8
+ lastStartAt: number | null;
9
+ lastStopAt: number | null;
10
+ lastError: string | null;
11
+ serverId: string | null;
12
+ endpoint: string | null;
13
+ bucket: string | null;
14
+ lastPollAt: number | null;
15
+ checkpointHeadKey: string | null;
16
+ lastInboundAt: number | null;
17
+ lastOutboundAt: number | null;
18
+ };
19
+ export type RelayPollLog = {
20
+ info?: (message: string, meta?: Record<string, unknown>) => void;
21
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
22
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
23
+ error?: (message: string, meta?: Record<string, unknown>) => void;
24
+ };
25
+ export interface DispatchInboundMessageParams {
26
+ cfg: Record<string, unknown>;
27
+ account: ResolvedR2RelayAccount;
28
+ service: Service;
29
+ log?: RelayPollLog;
30
+ senderId: string;
31
+ text: string;
32
+ timestamp: number;
33
+ messageId: string;
34
+ route: RelayMessage["route"];
35
+ attachments?: AttachmentRef[];
36
+ }
37
+ export interface PollRelayInboxParams {
38
+ cfg: Record<string, unknown>;
39
+ account: ResolvedR2RelayAccount;
40
+ service: Service;
41
+ abortSignal?: AbortSignal;
42
+ log?: RelayPollLog;
43
+ setStatus: (patch: unknown) => void;
44
+ state: RelayCheckpointState;
45
+ syncPublishedIdentity: (service: Service, account: ResolvedR2RelayAccount, force?: boolean) => Promise<void>;
46
+ dispatchInboundMessage: (params: DispatchInboundMessageParams) => Promise<void>;
47
+ formatInboundRelayBody: (msg: RelayMessage) => string;
48
+ classifyInboundRelayMessageFailure: (err: unknown) => "error";
49
+ }
50
+ export declare function createDefaultRuntimeState(accountId: string): RelayRuntimeState;
51
+ export declare function getRuntimeSnapshot(accountId: string): RelayRuntimeState | undefined;
52
+ export declare function setRuntimeSnapshot(accountId: string, snapshot: RelayRuntimeState): void;
53
+ export declare function touchRuntime(accountId: string, patch: Partial<RelayRuntimeState>): void;
54
+ export declare function getOrCreateService(account: ResolvedR2RelayAccount): Service;
55
+ export declare function clearService(accountId: string): void;
56
+ export declare function redactEndpoint(endpoint: string): string;
57
+ export declare function emitRelayDebug(log: RelayPollLog | undefined, message: string, meta?: Record<string, unknown>): void;
58
+ export declare function pollRelayInbox(params: PollRelayInboxParams): Promise<void>;
59
+ export declare function runSweeperLoop(params: {
60
+ account: ResolvedR2RelayAccount;
61
+ service: Service;
62
+ abortSignal?: AbortSignal;
63
+ log?: RelayPollLog;
64
+ }): Promise<void>;