@relaymessenger/openclaw-plugin 0.4.7-staging.0 → 0.4.7-staging.10
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/contracts/relay-v1.lock.json +6 -6
- package/dist/src/channel.js +1 -0
- package/dist/src/dispatch.js +3 -1
- package/dist/src/inbound.js +12 -1
- package/dist/src/outbound.js +28 -10
- package/package.json +2 -2
- package/src/channel.ts +4 -0
- package/src/dispatch.ts +3 -1
- package/src/inbound.ts +12 -1
- package/src/outbound.ts +31 -12
- package/src/types.ts +6 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"relayServer": {
|
|
3
3
|
"repository": "RelayMessenger/Relay-Server",
|
|
4
|
-
"commit": "
|
|
4
|
+
"commit": "eb83978b6b2c625da82471e4af16acad8de0e618",
|
|
5
5
|
"openapiPath": "contracts/developer/openapi.yaml",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "27698655d12500fb9cd2e10dbf1c94025fbc64c288df6151db673a7649877111"
|
|
7
7
|
},
|
|
8
8
|
"relaySdk": {
|
|
9
9
|
"package": "@relaymessenger/sdk",
|
|
10
|
-
"version": "0.3.6-staging.
|
|
11
|
-
"integrity": "sha512-
|
|
12
|
-
"operationsSha256": "
|
|
13
|
-
"workspaceOpenapiSha256": "
|
|
10
|
+
"version": "0.3.6-staging.8",
|
|
11
|
+
"integrity": "sha512-Jj96lQtPRgn3y7BWBgAoestpSrA5JLoo91LUFPdUVj5jfbFwhOUzS3+GSp+1yd85c+r1pYjCAxh5mb5tiujmlw==",
|
|
12
|
+
"operationsSha256": "04e5e1f1f626a2b0607fb0d091f12a4205a9e298fdb6f56e5bfcc446c9fe5ee3",
|
|
13
|
+
"workspaceOpenapiSha256": "27698655d12500fb9cd2e10dbf1c94025fbc64c288df6151db673a7649877111",
|
|
14
14
|
"usedOperations": [
|
|
15
15
|
{
|
|
16
16
|
"method": "GET",
|
package/dist/src/channel.js
CHANGED
|
@@ -249,6 +249,7 @@ export const relayChannelPlugin = createChatChannelPlugin({
|
|
|
249
249
|
deliveryQueueId: ctx.deliveryQueueId,
|
|
250
250
|
deliveryPartIndex: ctx.deliveryPartIndex,
|
|
251
251
|
}),
|
|
252
|
+
onButtonsError: (error) => ctx.log?.warn?.(`relay: buttons block left as text: ${error}`),
|
|
252
253
|
...(ctx.onPlatformSendDispatch
|
|
253
254
|
? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
|
|
254
255
|
: {}),
|
package/dist/src/dispatch.js
CHANGED
|
@@ -170,7 +170,9 @@ export async function dispatchRelayEvent(params) {
|
|
|
170
170
|
reply: {
|
|
171
171
|
to: facts.chatId,
|
|
172
172
|
originatingTo: facts.chatId,
|
|
173
|
-
...(facts.
|
|
173
|
+
...((facts.replyAnchorId ?? facts.replyToId)
|
|
174
|
+
? { replyToId: facts.replyAnchorId ?? facts.replyToId }
|
|
175
|
+
: {}),
|
|
174
176
|
},
|
|
175
177
|
message: {
|
|
176
178
|
inboundEventKind: "user_request",
|
package/dist/src/inbound.js
CHANGED
|
@@ -8,6 +8,10 @@ function renderPart(part) {
|
|
|
8
8
|
return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
|
|
9
9
|
case "system":
|
|
10
10
|
return part.value;
|
|
11
|
+
// The agent's own buttons part reads as nothing, as on the server; its
|
|
12
|
+
// question is the text beside it. A tap arrives as ordinary text.
|
|
13
|
+
case "buttons":
|
|
14
|
+
return undefined;
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
export function renderRelayMessageParts(parts) {
|
|
@@ -61,7 +65,14 @@ export function buildRelayInboundFacts(event) {
|
|
|
61
65
|
? { ownerHandle: event.data.chat.owner_handle }
|
|
62
66
|
: {}),
|
|
63
67
|
...(event.data.reply_to?.message_id
|
|
64
|
-
? {
|
|
68
|
+
? {
|
|
69
|
+
replyToId: event.data.reply_to.message_id,
|
|
70
|
+
// The answer quotes the person's message, the one it answers (a bot's
|
|
71
|
+
// reply_to in Telegram and Discord names the person's message). A
|
|
72
|
+
// tap's reply_to names the agent's buttons part, which no reply may
|
|
73
|
+
// target, so this is also what keeps a tap answerable.
|
|
74
|
+
replyAnchorId: event.data.id,
|
|
75
|
+
}
|
|
65
76
|
: {}),
|
|
66
77
|
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
|
67
78
|
};
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { Relay, RelayAPIError, } from "@relaymessenger/sdk";
|
|
2
|
+
import { answerMessages, indexedIdempotencyKey, Relay, RelayAPIError, } from "@relaymessenger/sdk";
|
|
3
3
|
export const RELAY_TEXT_CHUNK_LIMIT = 10_000;
|
|
4
4
|
const IDEMPOTENCY_KEY_MAX_LENGTH = 255;
|
|
5
5
|
export function createRelaySdkClient(account) {
|
|
@@ -17,17 +17,35 @@ export function deriveRelayIdempotencyKey(params) {
|
|
|
17
17
|
? raw
|
|
18
18
|
: `relay-openclaw:sha256:${createHash("sha256").update(raw).digest("hex")}`;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* OpenClaw hands the agent's words as text, so buttons ride in them as the
|
|
22
|
+
* SDK's fenced block, lifted here into the buttons part, and a link written
|
|
23
|
+
* alone on a line goes out as its own link Message. A block that cannot be
|
|
24
|
+
* read stays in the words and is reported through `onButtonsError`. Without
|
|
25
|
+
* a block or a link line the words go exactly as OpenClaw handed them, in one
|
|
26
|
+
* Message; the response is the first Message's, the one the reply anchors to.
|
|
27
|
+
*/
|
|
20
28
|
export async function sendRelayText(params) {
|
|
21
29
|
await params.onPlatformSendDispatch?.();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const { messages, error } = answerMessages(params.text);
|
|
31
|
+
if (error)
|
|
32
|
+
params.onButtonsError?.(error);
|
|
33
|
+
if (messages.length === 0)
|
|
34
|
+
messages.push([{ type: "text", value: params.text }]);
|
|
35
|
+
let first;
|
|
36
|
+
for (const [index, parts] of messages.entries()) {
|
|
37
|
+
const response = await params.relay.chats.messages.send(params.chatId, {
|
|
38
|
+
message: {
|
|
39
|
+
parts,
|
|
40
|
+
idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
|
|
41
|
+
...(index === 0 && params.replyToId
|
|
42
|
+
? { reply_to: { message_id: params.replyToId } }
|
|
43
|
+
: {}),
|
|
44
|
+
},
|
|
45
|
+
}, params.signal ? { signal: params.signal } : undefined);
|
|
46
|
+
first ??= response;
|
|
47
|
+
}
|
|
48
|
+
return first;
|
|
31
49
|
}
|
|
32
50
|
export function classifyUnknownRelaySend(error) {
|
|
33
51
|
if (!(error instanceof RelayAPIError)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relaymessenger/openclaw-plugin",
|
|
3
|
-
"version": "0.4.7-staging.
|
|
3
|
+
"version": "0.4.7-staging.10",
|
|
4
4
|
"description": "Native Relay channel plugin for OpenClaw",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepack": "npm run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@relaymessenger/sdk": "0.3.6-staging.
|
|
40
|
+
"@relaymessenger/sdk": "0.3.6-staging.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.0.0",
|
package/src/channel.ts
CHANGED
|
@@ -329,6 +329,10 @@ export const relayChannelPlugin: ChannelPlugin<ResolvedRelayAccount> =
|
|
|
329
329
|
deliveryQueueId: ctx.deliveryQueueId,
|
|
330
330
|
deliveryPartIndex: ctx.deliveryPartIndex,
|
|
331
331
|
}),
|
|
332
|
+
onButtonsError: (error) =>
|
|
333
|
+
(ctx as { log?: { warn?: (message: string) => void } }).log?.warn?.(
|
|
334
|
+
`relay: buttons block left as text: ${error}`,
|
|
335
|
+
),
|
|
332
336
|
...(ctx.onPlatformSendDispatch
|
|
333
337
|
? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
|
|
334
338
|
: {}),
|
package/src/dispatch.ts
CHANGED
|
@@ -236,7 +236,9 @@ export async function dispatchRelayEvent(params: {
|
|
|
236
236
|
reply: {
|
|
237
237
|
to: facts.chatId,
|
|
238
238
|
originatingTo: facts.chatId,
|
|
239
|
-
...(facts.
|
|
239
|
+
...((facts.replyAnchorId ?? facts.replyToId)
|
|
240
|
+
? { replyToId: facts.replyAnchorId ?? facts.replyToId }
|
|
241
|
+
: {}),
|
|
240
242
|
},
|
|
241
243
|
message: {
|
|
242
244
|
inboundEventKind: "user_request",
|
package/src/inbound.ts
CHANGED
|
@@ -17,6 +17,10 @@ function renderPart(part: MessagePartResponse): string | undefined {
|
|
|
17
17
|
return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
|
|
18
18
|
case "system":
|
|
19
19
|
return part.value;
|
|
20
|
+
// The agent's own buttons part reads as nothing, as on the server; its
|
|
21
|
+
// question is the text beside it. A tap arrives as ordinary text.
|
|
22
|
+
case "buttons":
|
|
23
|
+
return undefined;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -84,7 +88,14 @@ export function buildRelayInboundFacts(
|
|
|
84
88
|
? { ownerHandle: event.data.chat.owner_handle }
|
|
85
89
|
: {}),
|
|
86
90
|
...(event.data.reply_to?.message_id
|
|
87
|
-
? {
|
|
91
|
+
? {
|
|
92
|
+
replyToId: event.data.reply_to.message_id,
|
|
93
|
+
// The answer quotes the person's message, the one it answers (a bot's
|
|
94
|
+
// reply_to in Telegram and Discord names the person's message). A
|
|
95
|
+
// tap's reply_to names the agent's buttons part, which no reply may
|
|
96
|
+
// target, so this is also what keeps a tap answerable.
|
|
97
|
+
replyAnchorId: event.data.id,
|
|
98
|
+
}
|
|
88
99
|
: {}),
|
|
89
100
|
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
|
90
101
|
};
|
package/src/outbound.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
answerMessages,
|
|
4
|
+
indexedIdempotencyKey,
|
|
3
5
|
Relay,
|
|
4
6
|
RelayAPIError,
|
|
5
7
|
type MessageSendResponse,
|
|
@@ -32,6 +34,14 @@ export function deriveRelayIdempotencyKey(params: {
|
|
|
32
34
|
: `relay-openclaw:sha256:${createHash("sha256").update(raw).digest("hex")}`;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* OpenClaw hands the agent's words as text, so buttons ride in them as the
|
|
39
|
+
* SDK's fenced block, lifted here into the buttons part, and a link written
|
|
40
|
+
* alone on a line goes out as its own link Message. A block that cannot be
|
|
41
|
+
* read stays in the words and is reported through `onButtonsError`. Without
|
|
42
|
+
* a block or a link line the words go exactly as OpenClaw handed them, in one
|
|
43
|
+
* Message; the response is the first Message's, the one the reply anchors to.
|
|
44
|
+
*/
|
|
35
45
|
export async function sendRelayText(params: {
|
|
36
46
|
relay: Pick<Relay, "chats">;
|
|
37
47
|
chatId: string;
|
|
@@ -40,21 +50,30 @@ export async function sendRelayText(params: {
|
|
|
40
50
|
idempotencyKey: string;
|
|
41
51
|
signal?: AbortSignal;
|
|
42
52
|
onPlatformSendDispatch?: () => Promise<void>;
|
|
53
|
+
onButtonsError?: (error: string) => void;
|
|
43
54
|
}): Promise<MessageSendResponse> {
|
|
44
55
|
await params.onPlatformSendDispatch?.();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const { messages, error } = answerMessages(params.text);
|
|
57
|
+
if (error) params.onButtonsError?.(error);
|
|
58
|
+
if (messages.length === 0) messages.push([{ type: "text", value: params.text }]);
|
|
59
|
+
let first: MessageSendResponse | undefined;
|
|
60
|
+
for (const [index, parts] of messages.entries()) {
|
|
61
|
+
const response = await params.relay.chats.messages.send(
|
|
62
|
+
params.chatId,
|
|
63
|
+
{
|
|
64
|
+
message: {
|
|
65
|
+
parts,
|
|
66
|
+
idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
|
|
67
|
+
...(index === 0 && params.replyToId
|
|
68
|
+
? { reply_to: { message_id: params.replyToId } }
|
|
69
|
+
: {}),
|
|
70
|
+
},
|
|
54
71
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
params.signal ? { signal: params.signal } : undefined,
|
|
73
|
+
);
|
|
74
|
+
first ??= response;
|
|
75
|
+
}
|
|
76
|
+
return first!;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
export function classifyUnknownRelaySend(error: unknown): {
|
package/src/types.ts
CHANGED
|
@@ -72,5 +72,11 @@ export type RelayInboundFacts = {
|
|
|
72
72
|
mentionHandles: string[];
|
|
73
73
|
ownerHandle?: ChatHandle;
|
|
74
74
|
replyToId?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The Message an outbound reply should quote when the person's Message
|
|
77
|
+
* was itself a reply: the person's Message. A tap's reply_to names the
|
|
78
|
+
* agent's buttons part, which no reply may target.
|
|
79
|
+
*/
|
|
80
|
+
replyAnchorId?: string;
|
|
75
81
|
timestamp?: number;
|
|
76
82
|
};
|