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.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +271 -0
- package/dist/checkpoint-store.d.ts +4 -0
- package/dist/checkpoint-store.js +16 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +54 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.js +123 -0
- package/dist/conversation-targets.d.ts +32 -0
- package/dist/conversation-targets.js +120 -0
- package/dist/inbound.d.ts +38 -0
- package/dist/inbound.js +440 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +26 -0
- package/dist/lifecycle.d.ts +64 -0
- package/dist/lifecycle.js +296 -0
- package/dist/media.d.ts +30 -0
- package/dist/media.js +361 -0
- package/dist/message-adapter.d.ts +27 -0
- package/dist/message-adapter.js +53 -0
- package/dist/openclaw.plugin.json +41 -0
- package/dist/outbound.d.ts +28 -0
- package/dist/outbound.js +141 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +1 -0
- package/dist/r2-relay.d.ts +1 -0
- package/dist/r2-relay.js +1 -0
- package/dist/r2client.d.ts +33 -0
- package/dist/r2client.js +137 -0
- package/dist/relay-core/checkpoint.d.ts +31 -0
- package/dist/relay-core/checkpoint.js +95 -0
- package/dist/relay-core/errors.d.ts +9 -0
- package/dist/relay-core/errors.js +18 -0
- package/dist/relay-core/index.d.ts +6 -0
- package/dist/relay-core/index.js +6 -0
- package/dist/relay-core/keyspace.d.ts +24 -0
- package/dist/relay-core/keyspace.js +60 -0
- package/dist/relay-core/object-store.d.ts +20 -0
- package/dist/relay-core/object-store.js +1 -0
- package/dist/relay-core/transport.d.ts +48 -0
- package/dist/relay-core/transport.js +326 -0
- package/dist/relay-core/types.d.ts +220 -0
- package/dist/relay-core/types.js +1 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +54 -0
- package/dist/service.d.ts +51 -0
- package/dist/service.js +186 -0
- package/dist/service.test.d.ts +1 -0
- package/dist/service.test.js +27 -0
- package/dist/session-publication.d.ts +7 -0
- package/dist/session-publication.js +365 -0
- package/dist/setup-entry.d.ts +6 -0
- package/dist/setup-entry.js +49 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +291 -0
- package/dist/target.d.ts +6 -0
- package/dist/target.js +53 -0
- package/dist/webhook.d.ts +18 -0
- package/dist/webhook.js +225 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +62 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
export const MAX_MS = 9999999999999;
|
|
3
|
+
const SAFE_FRAGMENT_RE = /[^a-zA-Z0-9._-]/g;
|
|
4
|
+
export function sanitizeFragment(value) {
|
|
5
|
+
return String(value ?? "").replace(SAFE_FRAGMENT_RE, "");
|
|
6
|
+
}
|
|
7
|
+
export function padRevTs(ts) {
|
|
8
|
+
return String(MAX_MS - Math.trunc(ts)).padStart(13, "0");
|
|
9
|
+
}
|
|
10
|
+
export function extractTimestampFromRelayKey(key) {
|
|
11
|
+
const name = key.split("/").pop()?.trim() ?? "";
|
|
12
|
+
const match = name.match(/^(\d{13})-/);
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const reversed = Number(match[1]);
|
|
17
|
+
if (!Number.isFinite(reversed)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return MAX_MS - reversed;
|
|
21
|
+
}
|
|
22
|
+
export class RelayKeyspace {
|
|
23
|
+
bucket;
|
|
24
|
+
idFactory;
|
|
25
|
+
nowMsFactory;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.bucket = options.bucket ?? "";
|
|
28
|
+
this.idFactory = options.idFactory ?? (() => randomBytes(4).toString("hex"));
|
|
29
|
+
this.nowMsFactory = options.nowMsFactory ?? (() => Date.now());
|
|
30
|
+
}
|
|
31
|
+
padRevTs(ts) {
|
|
32
|
+
return padRevTs(ts);
|
|
33
|
+
}
|
|
34
|
+
shortUuid() {
|
|
35
|
+
return this.idFactory();
|
|
36
|
+
}
|
|
37
|
+
makeMsgKey(recipient, nowMs, suffix) {
|
|
38
|
+
const ts = nowMs ?? this.nowMsFactory();
|
|
39
|
+
return `msg/${recipient}/${this.padRevTs(ts)}-${suffix ?? this.shortUuid()}.json`;
|
|
40
|
+
}
|
|
41
|
+
makeAttKey(recipient, messageId, index, name, nowMs) {
|
|
42
|
+
const ts = nowMs ?? this.nowMsFactory();
|
|
43
|
+
const safeMessageId = sanitizeFragment(messageId);
|
|
44
|
+
const safeIndex = String(index).padStart(2, "0");
|
|
45
|
+
const safeName = sanitizeFragment(name);
|
|
46
|
+
const suffix = safeName ? `-${safeName}` : "";
|
|
47
|
+
return `att/${recipient}/${this.padRevTs(ts)}-${safeMessageId}-${safeIndex}${suffix}`;
|
|
48
|
+
}
|
|
49
|
+
makeHeadKey(recipient) {
|
|
50
|
+
return `head/${recipient}.json`;
|
|
51
|
+
}
|
|
52
|
+
makeIdentityKey(peer) {
|
|
53
|
+
return `identity/${peer}.json`;
|
|
54
|
+
}
|
|
55
|
+
makeIdentifyKey(peer) {
|
|
56
|
+
return this.makeIdentityKey(peer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class R2Relay extends RelayKeyspace {
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ListPrefixItem {
|
|
2
|
+
Key?: string;
|
|
3
|
+
LastModified?: Date | null;
|
|
4
|
+
}
|
|
5
|
+
export interface ListPrefixPageResult {
|
|
6
|
+
contents: ListPrefixItem[];
|
|
7
|
+
nextContinuationToken: string | null;
|
|
8
|
+
isTruncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ObjectStore {
|
|
11
|
+
putObject(key: string, body: Buffer | string, contentType?: string, tagging?: string, ifMatch?: string, ifNoneMatch?: string): Promise<unknown>;
|
|
12
|
+
getObject(key: string): Promise<unknown | null>;
|
|
13
|
+
getJsonWithEtag(key: string): Promise<{
|
|
14
|
+
body: unknown;
|
|
15
|
+
etag: string | null;
|
|
16
|
+
} | null>;
|
|
17
|
+
deleteObject(key: string): Promise<unknown>;
|
|
18
|
+
deleteObjects(keys: string[]): Promise<unknown>;
|
|
19
|
+
listPrefixPage(prefix: string, continuationToken?: string, maxKeys?: number): Promise<ListPrefixPageResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CheckpointStore } from "./checkpoint.js";
|
|
2
|
+
import { RelayKeyspace } from "./keyspace.js";
|
|
3
|
+
import { ObjectStore } from "./object-store.js";
|
|
4
|
+
import { HeadDoc, IdentityDoc, InboxBatch, RelayMessage, RelayMessageStatus, RelayTextContent, RelayRetentionConfig, SendMessageOptions, SendMessageResult, SweepRuleSummary } from "./types.js";
|
|
5
|
+
export interface RelayTransportOptions {
|
|
6
|
+
store: ObjectStore;
|
|
7
|
+
keyspace?: RelayKeyspace;
|
|
8
|
+
peerId: string;
|
|
9
|
+
checkpointStore?: CheckpointStore;
|
|
10
|
+
sleep?: (ms: number) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare class R2RelayTransport {
|
|
13
|
+
readonly store: ObjectStore;
|
|
14
|
+
readonly keyspace: RelayKeyspace;
|
|
15
|
+
readonly peerId: string;
|
|
16
|
+
readonly checkpointStore: CheckpointStore;
|
|
17
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
18
|
+
private readonly sendLanes;
|
|
19
|
+
constructor(options: RelayTransportOptions);
|
|
20
|
+
publishIdentity(identity?: Partial<IdentityDoc>): Promise<IdentityDoc>;
|
|
21
|
+
getHeadState(peer: string): Promise<{
|
|
22
|
+
doc: HeadDoc;
|
|
23
|
+
etag: string | null;
|
|
24
|
+
} | null>;
|
|
25
|
+
getHead(peer: string): Promise<HeadDoc | null>;
|
|
26
|
+
readMessage(key: string): Promise<RelayMessage | null>;
|
|
27
|
+
sendMessage(to: string, options: SendMessageOptions): Promise<SendMessageResult>;
|
|
28
|
+
private sendMessageUnlocked;
|
|
29
|
+
sendStreamingSnapshots(to: string, snapshots: string[], options: {
|
|
30
|
+
route: SendMessageOptions["route"];
|
|
31
|
+
streamId?: string | null;
|
|
32
|
+
attachments?: RelayTextContent["attachments"];
|
|
33
|
+
}): Promise<{
|
|
34
|
+
streamId: string;
|
|
35
|
+
results: SendMessageResult[];
|
|
36
|
+
}>;
|
|
37
|
+
markMessageProcessed(key: string, patch: {
|
|
38
|
+
processedAt?: number;
|
|
39
|
+
processedBy?: string | null;
|
|
40
|
+
state?: RelayMessageStatus["state"];
|
|
41
|
+
error?: string | null;
|
|
42
|
+
}): Promise<RelayMessage | null>;
|
|
43
|
+
collectInboxMessages(selfId: string, lastSeenKey: string | null): Promise<InboxBatch>;
|
|
44
|
+
sweepByKeyTimestamp(prefix: string, ttlDays: number, abortSignal?: AbortSignal): Promise<SweepRuleSummary>;
|
|
45
|
+
sweepByLastModified(prefix: string, ttlDays: number, abortSignal?: AbortSignal): Promise<SweepRuleSummary>;
|
|
46
|
+
sweepRetention(ttl: RelayRetentionConfig, abortSignal?: AbortSignal): Promise<SweepRuleSummary[]>;
|
|
47
|
+
pollInbox(selfId: string, handler: (msg: RelayMessage, key: string) => Promise<void>, pollIntervalMs?: number, backoffMax?: number, deleteAfterProcessing?: boolean, abortSignal?: AbortSignal): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { CASRetryExceededError, PreconditionFailedError } from "./errors.js";
|
|
2
|
+
import { defaultCheckpointState, hasSeenMessage, InMemoryCheckpointStore, rememberMessage } from "./checkpoint.js";
|
|
3
|
+
import { extractTimestampFromRelayKey, RelayKeyspace } from "./keyspace.js";
|
|
4
|
+
async function readBodyAsText(body) {
|
|
5
|
+
if (typeof body === "string") {
|
|
6
|
+
return body;
|
|
7
|
+
}
|
|
8
|
+
if (!body) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
const value = body;
|
|
12
|
+
if (typeof value.transformToString === "function") {
|
|
13
|
+
return await value.transformToString();
|
|
14
|
+
}
|
|
15
|
+
if (typeof value[Symbol.asyncIterator] === "function") {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
for await (const chunk of value) {
|
|
18
|
+
chunks.push(Buffer.from(chunk));
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
21
|
+
}
|
|
22
|
+
return String(body);
|
|
23
|
+
}
|
|
24
|
+
export class R2RelayTransport {
|
|
25
|
+
store;
|
|
26
|
+
keyspace;
|
|
27
|
+
peerId;
|
|
28
|
+
checkpointStore;
|
|
29
|
+
sleep;
|
|
30
|
+
sendLanes = new Map();
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.store = options.store;
|
|
33
|
+
this.keyspace = options.keyspace ?? new RelayKeyspace();
|
|
34
|
+
this.peerId = options.peerId;
|
|
35
|
+
this.checkpointStore = options.checkpointStore ?? new InMemoryCheckpointStore(defaultCheckpointState());
|
|
36
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
37
|
+
}
|
|
38
|
+
async publishIdentity(identity) {
|
|
39
|
+
const doc = {
|
|
40
|
+
display_name: this.peerId,
|
|
41
|
+
role: "server",
|
|
42
|
+
last_seen: this.keyspace.nowMsFactory(),
|
|
43
|
+
protocol: { name: "r2-relay", version: 3 },
|
|
44
|
+
software: { id: "unknown" },
|
|
45
|
+
capabilities: {
|
|
46
|
+
messaging: { text: true, streaming: false, reactions: false, system_events: false },
|
|
47
|
+
conversations: { list: false, create: false, reset: false, archive: false, threading: false },
|
|
48
|
+
agents: { list: false, multiple: false, switch: false, per_agent_models: false },
|
|
49
|
+
},
|
|
50
|
+
limits: null,
|
|
51
|
+
agents: [],
|
|
52
|
+
conversations: [],
|
|
53
|
+
...(identity ?? {}),
|
|
54
|
+
peer: identity?.peer ?? this.peerId,
|
|
55
|
+
};
|
|
56
|
+
await this.store.putObject(this.keyspace.makeIdentityKey(doc.peer), JSON.stringify(doc), "application/json");
|
|
57
|
+
return doc;
|
|
58
|
+
}
|
|
59
|
+
async getHeadState(peer) {
|
|
60
|
+
const result = await this.store.getJsonWithEtag(this.keyspace.makeHeadKey(peer));
|
|
61
|
+
return result ? { doc: result.body, etag: result.etag } : null;
|
|
62
|
+
}
|
|
63
|
+
async getHead(peer) {
|
|
64
|
+
return (await this.getHeadState(peer))?.doc ?? null;
|
|
65
|
+
}
|
|
66
|
+
async readMessage(key) {
|
|
67
|
+
const response = await this.store.getObject(key);
|
|
68
|
+
if (!response) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof response === "object" && response !== null && "payload" in response) {
|
|
72
|
+
const payload = response.payload;
|
|
73
|
+
if (typeof payload === "string") {
|
|
74
|
+
return JSON.parse(payload);
|
|
75
|
+
}
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
if (typeof response === "object" && response !== null && "Body" in response) {
|
|
79
|
+
return JSON.parse(await readBodyAsText(response.Body));
|
|
80
|
+
}
|
|
81
|
+
if (typeof response === "string") {
|
|
82
|
+
return JSON.parse(response);
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
async sendMessage(to, options) {
|
|
87
|
+
const previous = this.sendLanes.get(to) ?? Promise.resolve({ key: "", messageId: "" });
|
|
88
|
+
const current = previous.catch(() => ({ key: "", messageId: "" })).then(() => this.sendMessageUnlocked(to, options));
|
|
89
|
+
this.sendLanes.set(to, current);
|
|
90
|
+
try {
|
|
91
|
+
return await current;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (this.sendLanes.get(to) === current) {
|
|
95
|
+
this.sendLanes.delete(to);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async sendMessageUnlocked(to, options) {
|
|
100
|
+
const now = this.keyspace.nowMsFactory();
|
|
101
|
+
const headKey = this.keyspace.makeHeadKey(to);
|
|
102
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
103
|
+
const currentHead = await this.getHeadState(to);
|
|
104
|
+
const prevKey = currentHead?.doc.head_key ?? null;
|
|
105
|
+
const key = this.keyspace.makeMsgKey(to, now);
|
|
106
|
+
const msgId = this.keyspace.shortUuid();
|
|
107
|
+
const msg = {
|
|
108
|
+
msg_id: msgId,
|
|
109
|
+
from: this.peerId,
|
|
110
|
+
to,
|
|
111
|
+
ts_sent: now,
|
|
112
|
+
prev_key: prevKey,
|
|
113
|
+
route: options.route,
|
|
114
|
+
content: options.content,
|
|
115
|
+
delivery: options.delivery ?? null,
|
|
116
|
+
status: null,
|
|
117
|
+
size: options.content.type === "text"
|
|
118
|
+
? Buffer.byteLength(options.content.text ?? "")
|
|
119
|
+
: null,
|
|
120
|
+
};
|
|
121
|
+
const newHead = {
|
|
122
|
+
head_key: key,
|
|
123
|
+
head_msg_id: msgId,
|
|
124
|
+
head_ts: now,
|
|
125
|
+
};
|
|
126
|
+
await this.store.putObject(key, JSON.stringify(msg), "application/json", undefined, undefined, "*");
|
|
127
|
+
try {
|
|
128
|
+
if (!currentHead?.doc) {
|
|
129
|
+
await this.store.putObject(headKey, JSON.stringify(newHead), "application/json", undefined, undefined, "*");
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (!currentHead.etag) {
|
|
133
|
+
throw new Error("Missing head ETag for CAS update");
|
|
134
|
+
}
|
|
135
|
+
await this.store.putObject(headKey, JSON.stringify(newHead), "application/json", undefined, currentHead.etag, undefined);
|
|
136
|
+
}
|
|
137
|
+
return { key, messageId: msgId };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (!(err instanceof PreconditionFailedError)) {
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
await this.sleep(20);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new CASRetryExceededError();
|
|
147
|
+
}
|
|
148
|
+
async sendStreamingSnapshots(to, snapshots, options) {
|
|
149
|
+
const streamId = options.streamId ?? this.keyspace.shortUuid();
|
|
150
|
+
const results = [];
|
|
151
|
+
const nonEmpty = snapshots.filter((s) => (s ?? "").trim());
|
|
152
|
+
for (let index = 0; index < nonEmpty.length; index += 1) {
|
|
153
|
+
const text = nonEmpty[index] ?? "";
|
|
154
|
+
const result = await this.sendMessage(to, {
|
|
155
|
+
route: options.route,
|
|
156
|
+
content: {
|
|
157
|
+
type: "text",
|
|
158
|
+
text,
|
|
159
|
+
...(options.attachments?.length ? { attachments: options.attachments } : {}),
|
|
160
|
+
},
|
|
161
|
+
delivery: {
|
|
162
|
+
stream: {
|
|
163
|
+
stream_id: streamId,
|
|
164
|
+
seq: index + 1,
|
|
165
|
+
state: index === nonEmpty.length - 1 ? "final" : "partial",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
results.push(result);
|
|
170
|
+
}
|
|
171
|
+
return { streamId, results };
|
|
172
|
+
}
|
|
173
|
+
async markMessageProcessed(key, patch) {
|
|
174
|
+
const msg = await this.readMessage(key);
|
|
175
|
+
if (!msg) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const updated = {
|
|
179
|
+
...msg,
|
|
180
|
+
status: {
|
|
181
|
+
state: patch.state ?? "done",
|
|
182
|
+
processed_at: patch.processedAt ?? this.keyspace.nowMsFactory(),
|
|
183
|
+
processed_by: patch.processedBy ?? this.peerId,
|
|
184
|
+
error: patch.error ?? null,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
await this.store.putObject(key, JSON.stringify(updated), "application/json");
|
|
188
|
+
return updated;
|
|
189
|
+
}
|
|
190
|
+
async collectInboxMessages(selfId, lastSeenKey) {
|
|
191
|
+
const head = await this.getHead(selfId);
|
|
192
|
+
if (!head?.head_key || head.head_key === lastSeenKey) {
|
|
193
|
+
return { head, messages: [], checkpointHeadKey: lastSeenKey };
|
|
194
|
+
}
|
|
195
|
+
let current = head.head_key;
|
|
196
|
+
const window = [];
|
|
197
|
+
const visitedKeys = new Set();
|
|
198
|
+
while (current && current !== lastSeenKey) {
|
|
199
|
+
if (visitedKeys.has(current)) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
visitedKeys.add(current);
|
|
203
|
+
const msg = await this.readMessage(current);
|
|
204
|
+
if (!msg) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
window.push({ key: current, message: msg });
|
|
208
|
+
if (window.length > 500) {
|
|
209
|
+
window.shift();
|
|
210
|
+
}
|
|
211
|
+
current = msg.prev_key ?? null;
|
|
212
|
+
}
|
|
213
|
+
window.reverse();
|
|
214
|
+
return {
|
|
215
|
+
head,
|
|
216
|
+
messages: window,
|
|
217
|
+
checkpointHeadKey: window[window.length - 1]?.key ?? lastSeenKey,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async sweepByKeyTimestamp(prefix, ttlDays, abortSignal) {
|
|
221
|
+
const cutoffTs = this.keyspace.nowMsFactory() - ttlDays * 24 * 60 * 60 * 1000;
|
|
222
|
+
let continuationToken;
|
|
223
|
+
let scanned = 0;
|
|
224
|
+
let deleted = 0;
|
|
225
|
+
while (!abortSignal?.aborted) {
|
|
226
|
+
const page = await this.store.listPrefixPage(prefix, continuationToken, 1000);
|
|
227
|
+
const toDelete = [];
|
|
228
|
+
for (const item of page.contents) {
|
|
229
|
+
const key = item.Key;
|
|
230
|
+
if (!key)
|
|
231
|
+
continue;
|
|
232
|
+
scanned += 1;
|
|
233
|
+
const ts = extractTimestampFromRelayKey(key);
|
|
234
|
+
if (ts !== null && ts < cutoffTs) {
|
|
235
|
+
toDelete.push(key);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (toDelete.length > 0) {
|
|
239
|
+
await this.store.deleteObjects(toDelete);
|
|
240
|
+
deleted += toDelete.length;
|
|
241
|
+
}
|
|
242
|
+
if (!page.isTruncated || !page.nextContinuationToken) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
continuationToken = page.nextContinuationToken;
|
|
246
|
+
}
|
|
247
|
+
return { prefix, scanned, deleted };
|
|
248
|
+
}
|
|
249
|
+
async sweepByLastModified(prefix, ttlDays, abortSignal) {
|
|
250
|
+
const cutoffTs = this.keyspace.nowMsFactory() - ttlDays * 24 * 60 * 60 * 1000;
|
|
251
|
+
let continuationToken;
|
|
252
|
+
let scanned = 0;
|
|
253
|
+
let deleted = 0;
|
|
254
|
+
while (!abortSignal?.aborted) {
|
|
255
|
+
const page = await this.store.listPrefixPage(prefix, continuationToken, 1000);
|
|
256
|
+
const toDelete = [];
|
|
257
|
+
for (const item of page.contents) {
|
|
258
|
+
const key = item.Key;
|
|
259
|
+
if (!key)
|
|
260
|
+
continue;
|
|
261
|
+
scanned += 1;
|
|
262
|
+
const lastModified = item.LastModified?.getTime?.() ?? null;
|
|
263
|
+
if (lastModified !== null && lastModified < cutoffTs) {
|
|
264
|
+
toDelete.push(key);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (toDelete.length > 0) {
|
|
268
|
+
await this.store.deleteObjects(toDelete);
|
|
269
|
+
deleted += toDelete.length;
|
|
270
|
+
}
|
|
271
|
+
if (!page.isTruncated || !page.nextContinuationToken) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
continuationToken = page.nextContinuationToken;
|
|
275
|
+
}
|
|
276
|
+
return { prefix, scanned, deleted };
|
|
277
|
+
}
|
|
278
|
+
async sweepRetention(ttl, abortSignal) {
|
|
279
|
+
const summaries = [];
|
|
280
|
+
if ((ttl.msg ?? 0) > 0) {
|
|
281
|
+
summaries.push(await this.sweepByKeyTimestamp("msg/", ttl.msg, abortSignal));
|
|
282
|
+
}
|
|
283
|
+
if ((ttl.att ?? 0) > 0) {
|
|
284
|
+
summaries.push(await this.sweepByKeyTimestamp("att/", ttl.att, abortSignal));
|
|
285
|
+
}
|
|
286
|
+
if ((ttl.identity ?? 0) > 0) {
|
|
287
|
+
summaries.push(await this.sweepByLastModified("identity/", ttl.identity, abortSignal));
|
|
288
|
+
}
|
|
289
|
+
if ((ttl.head ?? 0) > 0) {
|
|
290
|
+
summaries.push(await this.sweepByLastModified("head/", ttl.head, abortSignal));
|
|
291
|
+
}
|
|
292
|
+
return summaries;
|
|
293
|
+
}
|
|
294
|
+
async pollInbox(selfId, handler, pollIntervalMs = 5000, backoffMax = 40000, deleteAfterProcessing = true, abortSignal) {
|
|
295
|
+
let interval = pollIntervalMs;
|
|
296
|
+
let state = await this.checkpointStore.load();
|
|
297
|
+
while (!abortSignal?.aborted) {
|
|
298
|
+
const batch = await this.collectInboxMessages(selfId, state.lastHeadKey);
|
|
299
|
+
if (batch.messages.length > 0) {
|
|
300
|
+
for (const item of batch.messages) {
|
|
301
|
+
if (hasSeenMessage(state, { msgId: item.message.msg_id, objectKey: item.key })) {
|
|
302
|
+
state = { ...state, lastHeadKey: item.key };
|
|
303
|
+
await this.checkpointStore.save(state);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
await handler(item.message, item.key);
|
|
307
|
+
state = rememberMessage(state, { msgId: item.message.msg_id, objectKey: item.key, at: item.message.ts_sent });
|
|
308
|
+
state = { ...state, lastHeadKey: item.key, lastPollAt: this.keyspace.nowMsFactory() };
|
|
309
|
+
await this.checkpointStore.save(state);
|
|
310
|
+
if (deleteAfterProcessing) {
|
|
311
|
+
await this.store.deleteObject(item.key);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
interval = pollIntervalMs;
|
|
315
|
+
await this.sleep(interval);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
state = { ...state, lastPollAt: this.keyspace.nowMsFactory() };
|
|
319
|
+
await this.checkpointStore.save(state);
|
|
320
|
+
await this.sleep(interval);
|
|
321
|
+
if (!batch.head?.head_key) {
|
|
322
|
+
interval = Math.min(interval * 2, backoffMax);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export type AttachmentKind = "image" | "video" | "audio" | "file" | "unknown";
|
|
2
|
+
export interface AttachmentRef {
|
|
3
|
+
id: string;
|
|
4
|
+
key: string;
|
|
5
|
+
file_name: string | null;
|
|
6
|
+
content_type: string | null;
|
|
7
|
+
size: number | null;
|
|
8
|
+
sha256: string | null;
|
|
9
|
+
kind: AttachmentKind;
|
|
10
|
+
width: number | null;
|
|
11
|
+
height: number | null;
|
|
12
|
+
duration_ms: number | null;
|
|
13
|
+
preview_image_key: string | null;
|
|
14
|
+
preview_image_type: string | null;
|
|
15
|
+
preview_size: number | null;
|
|
16
|
+
}
|
|
17
|
+
export interface RelayRoute {
|
|
18
|
+
agent_id: string;
|
|
19
|
+
conversation_id?: string | null;
|
|
20
|
+
instance_id?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface RelayTextContent {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
attachments?: AttachmentRef[] | null;
|
|
26
|
+
}
|
|
27
|
+
export interface RelayReactionContent {
|
|
28
|
+
type: "reaction";
|
|
29
|
+
target_msg_id: string;
|
|
30
|
+
emoji: string;
|
|
31
|
+
remove?: boolean | null;
|
|
32
|
+
}
|
|
33
|
+
export interface RelayApprovalRequestContent {
|
|
34
|
+
type: "approval_request";
|
|
35
|
+
approval_id: string;
|
|
36
|
+
approval_kind: "exec" | "tool" | "custom";
|
|
37
|
+
title: string;
|
|
38
|
+
body?: string | null;
|
|
39
|
+
allowed_decisions: string[];
|
|
40
|
+
metadata?: Record<string, unknown> | null;
|
|
41
|
+
}
|
|
42
|
+
export interface RelayApprovalResponseContent {
|
|
43
|
+
type: "approval_response";
|
|
44
|
+
approval_id: string;
|
|
45
|
+
decision: string;
|
|
46
|
+
metadata?: Record<string, unknown> | null;
|
|
47
|
+
}
|
|
48
|
+
export interface RelaySystemContent {
|
|
49
|
+
type: "system";
|
|
50
|
+
event: string;
|
|
51
|
+
data?: Record<string, unknown> | null;
|
|
52
|
+
}
|
|
53
|
+
export type RelayContent = RelayTextContent | RelayReactionContent | RelayApprovalRequestContent | RelayApprovalResponseContent | RelaySystemContent;
|
|
54
|
+
export interface RelayDeliveryStream {
|
|
55
|
+
stream_id: string;
|
|
56
|
+
seq: number;
|
|
57
|
+
state: "partial" | "final";
|
|
58
|
+
}
|
|
59
|
+
export interface RelayDelivery {
|
|
60
|
+
stream?: RelayDeliveryStream | null;
|
|
61
|
+
}
|
|
62
|
+
export interface RelayMessageStatus {
|
|
63
|
+
state: "pending" | "processing" | "done" | "error";
|
|
64
|
+
processed_at?: number | null;
|
|
65
|
+
processed_by?: string | null;
|
|
66
|
+
error?: string | null;
|
|
67
|
+
}
|
|
68
|
+
export interface RelayMessage {
|
|
69
|
+
msg_id: string;
|
|
70
|
+
from: string;
|
|
71
|
+
to: string;
|
|
72
|
+
ts_sent: number;
|
|
73
|
+
prev_key: string | null;
|
|
74
|
+
route: RelayRoute;
|
|
75
|
+
content: RelayContent;
|
|
76
|
+
delivery?: RelayDelivery | null;
|
|
77
|
+
status?: RelayMessageStatus | null;
|
|
78
|
+
size?: number | null;
|
|
79
|
+
}
|
|
80
|
+
export interface HeadDoc {
|
|
81
|
+
head_key: string;
|
|
82
|
+
head_msg_id: string;
|
|
83
|
+
head_ts: number;
|
|
84
|
+
}
|
|
85
|
+
export interface RelayCapabilities {
|
|
86
|
+
messaging: {
|
|
87
|
+
text: boolean;
|
|
88
|
+
streaming: boolean;
|
|
89
|
+
reactions: boolean;
|
|
90
|
+
system_events: boolean;
|
|
91
|
+
};
|
|
92
|
+
conversations: {
|
|
93
|
+
list: boolean;
|
|
94
|
+
create: boolean;
|
|
95
|
+
reset: boolean;
|
|
96
|
+
archive: boolean;
|
|
97
|
+
threading: boolean;
|
|
98
|
+
};
|
|
99
|
+
agents: {
|
|
100
|
+
list: boolean;
|
|
101
|
+
multiple: boolean;
|
|
102
|
+
switch: boolean;
|
|
103
|
+
per_agent_models: boolean;
|
|
104
|
+
};
|
|
105
|
+
attachments?: {
|
|
106
|
+
supported: boolean;
|
|
107
|
+
kinds: AttachmentKind[];
|
|
108
|
+
max_bytes_by_kind?: Partial<Record<AttachmentKind, number>>;
|
|
109
|
+
oversize_behavior?: "reject" | "drop" | "link" | "summarize";
|
|
110
|
+
} | null;
|
|
111
|
+
approvals?: {
|
|
112
|
+
exec: boolean;
|
|
113
|
+
tool: boolean;
|
|
114
|
+
custom: boolean;
|
|
115
|
+
} | null;
|
|
116
|
+
extensions?: Record<string, unknown> | null;
|
|
117
|
+
}
|
|
118
|
+
export interface ModelDescriptor {
|
|
119
|
+
id: string;
|
|
120
|
+
label?: string | null;
|
|
121
|
+
provider?: string | null;
|
|
122
|
+
}
|
|
123
|
+
export interface AgentDescriptor {
|
|
124
|
+
id: string;
|
|
125
|
+
display_name?: string | null;
|
|
126
|
+
description?: string | null;
|
|
127
|
+
is_default: boolean;
|
|
128
|
+
models?: {
|
|
129
|
+
available: ModelDescriptor[];
|
|
130
|
+
default?: string | null;
|
|
131
|
+
} | null;
|
|
132
|
+
default_route: RelayRoute;
|
|
133
|
+
capabilities?: Partial<RelayCapabilities> | null;
|
|
134
|
+
}
|
|
135
|
+
export interface ConversationSource {
|
|
136
|
+
channel: string;
|
|
137
|
+
chat_kind?: "dm" | "group" | "channel" | "thread" | null;
|
|
138
|
+
account_id?: string | null;
|
|
139
|
+
account_display?: string | null;
|
|
140
|
+
space_id?: string | null;
|
|
141
|
+
space_display?: string | null;
|
|
142
|
+
chat_id?: string | null;
|
|
143
|
+
chat_display?: string | null;
|
|
144
|
+
participant_id?: string | null;
|
|
145
|
+
participant_display?: string | null;
|
|
146
|
+
thread_id?: string | null;
|
|
147
|
+
thread_display?: string | null;
|
|
148
|
+
sharing?: "private" | "shared" | "per-user" | null;
|
|
149
|
+
}
|
|
150
|
+
export interface ConversationDescriptor {
|
|
151
|
+
id: string;
|
|
152
|
+
display_title?: string | null;
|
|
153
|
+
route: RelayRoute;
|
|
154
|
+
source?: ConversationSource | null;
|
|
155
|
+
updated_at?: number | null;
|
|
156
|
+
}
|
|
157
|
+
export interface ServerLimits {
|
|
158
|
+
inbound_attachment_max_bytes?: {
|
|
159
|
+
image: number;
|
|
160
|
+
video: number;
|
|
161
|
+
audio: number;
|
|
162
|
+
file: number;
|
|
163
|
+
} | null;
|
|
164
|
+
oversize_attachment_behavior?: string | null;
|
|
165
|
+
}
|
|
166
|
+
export interface IdentityDoc {
|
|
167
|
+
peer: string;
|
|
168
|
+
display_name: string;
|
|
169
|
+
role: "server" | "client";
|
|
170
|
+
last_seen: number;
|
|
171
|
+
protocol: {
|
|
172
|
+
name: "r2-relay";
|
|
173
|
+
version: number;
|
|
174
|
+
};
|
|
175
|
+
software: {
|
|
176
|
+
id: string;
|
|
177
|
+
name?: string | null;
|
|
178
|
+
version?: string | null;
|
|
179
|
+
};
|
|
180
|
+
capabilities: RelayCapabilities;
|
|
181
|
+
limits?: ServerLimits | null;
|
|
182
|
+
agents: AgentDescriptor[];
|
|
183
|
+
conversations: ConversationDescriptor[];
|
|
184
|
+
}
|
|
185
|
+
export interface InboxMessage {
|
|
186
|
+
key: string;
|
|
187
|
+
message: RelayMessage;
|
|
188
|
+
}
|
|
189
|
+
export interface InboxBatch {
|
|
190
|
+
head: HeadDoc | null;
|
|
191
|
+
messages: InboxMessage[];
|
|
192
|
+
checkpointHeadKey: string | null;
|
|
193
|
+
}
|
|
194
|
+
export interface SendMessageOptions {
|
|
195
|
+
route: RelayRoute;
|
|
196
|
+
content: RelayContent;
|
|
197
|
+
delivery?: RelayDelivery | null;
|
|
198
|
+
}
|
|
199
|
+
export interface SendMessageResult {
|
|
200
|
+
key: string;
|
|
201
|
+
messageId: string;
|
|
202
|
+
}
|
|
203
|
+
export interface RelayRetentionConfig {
|
|
204
|
+
msg?: number;
|
|
205
|
+
att?: number;
|
|
206
|
+
identity?: number;
|
|
207
|
+
head?: number;
|
|
208
|
+
}
|
|
209
|
+
export interface SweepRuleSummary {
|
|
210
|
+
prefix: string;
|
|
211
|
+
scanned: number;
|
|
212
|
+
deleted: number;
|
|
213
|
+
}
|
|
214
|
+
export interface RelayCheckpointState {
|
|
215
|
+
lastHeadKey: string | null;
|
|
216
|
+
recentMessageIds: string[];
|
|
217
|
+
recentObjectKeys: string[];
|
|
218
|
+
lastPollAt: number | null;
|
|
219
|
+
lastInboundAt: number | null;
|
|
220
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
|
+
declare const setRelayRuntime: (next: PluginRuntime) => void, getRelayRuntime: () => PluginRuntime;
|
|
4
|
+
declare function setRelayConfig(cfg: OpenClawConfig): void;
|
|
5
|
+
declare function getRelayConfig(): OpenClawConfig;
|
|
6
|
+
declare function setRelayStateDir(stateDir: string): void;
|
|
7
|
+
declare function resolvePluginStateDir(): string;
|
|
8
|
+
declare function getPersistedRelayServerId(): string;
|
|
9
|
+
export { getPersistedRelayServerId, getRelayConfig, getRelayRuntime, resolvePluginStateDir, setRelayConfig, setRelayRuntime, setRelayStateDir };
|