@relaymessenger/openclaw-plugin 0.4.7-staging.3 → 0.4.7-staging.31
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 +22 -0
- package/contracts/relay-v1.lock.json +6 -6
- package/dist/src/channel.js +1 -0
- package/dist/src/dispatch.js +4 -1
- package/dist/src/inbound.js +16 -9
- package/dist/src/outbound.js +42 -10
- package/package.json +5 -5
- package/src/channel.ts +4 -0
- package/src/dispatch.ts +4 -1
- package/src/inbound.ts +16 -9
- package/src/outbound.ts +45 -13
- package/src/types.ts +8 -4
package/README.md
CHANGED
|
@@ -12,6 +12,28 @@ delivers events over its v1 WebSocket, and the plugin sends replies through
|
|
|
12
12
|
the Relay v1 REST Message API. The plugin imports `@relaymessenger/sdk`; it
|
|
13
13
|
does not contain a copied Relay client or protocol implementation.
|
|
14
14
|
|
|
15
|
+
## Selection
|
|
16
|
+
|
|
17
|
+
End the final answer with a `selection` JSON fence after the question.
|
|
18
|
+
`BodyForAgent` carries structured response and rich-message JSON; `RawBody` and
|
|
19
|
+
`CommandBody` retain readable text. Stable values are not executable commands.
|
|
20
|
+
|
|
21
|
+
New human reply text is literal `• ` + each selected source label joined with
|
|
22
|
+
`\n`, followed by `selection_response` metadata in source-option order. Dispatch
|
|
23
|
+
with `selected_values` and the explicit source target, never label parsing.
|
|
24
|
+
Exact legacy comma-joined text remains a server compatibility input. The person
|
|
25
|
+
checks any number of options and submits them once; checking sends nothing, and
|
|
26
|
+
a person answers a given selection once. iOS may draw a checkmark in place of
|
|
27
|
+
each bullet and repeat the prompt's title, as presentation only.
|
|
28
|
+
|
|
29
|
+
## Payment
|
|
30
|
+
|
|
31
|
+
The agent ends the final answer with a `payment` JSON fence holding the
|
|
32
|
+
payment request's fields (`description`, `category`, and `amount` with
|
|
33
|
+
`currency`, or `mode: "subscription"` with `price_id`). The plugin creates the
|
|
34
|
+
request with its own Relay token, on the card's own idempotency key; the words
|
|
35
|
+
go first and the payment card follows as its own Message.
|
|
36
|
+
|
|
15
37
|
## Install
|
|
16
38
|
|
|
17
39
|
```bash
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"relayServer": {
|
|
3
3
|
"repository": "RelayMessenger/Relay-Server",
|
|
4
|
-
"commit": "
|
|
4
|
+
"commit": "486fb8b70680bafd52532d478e4067e714901774",
|
|
5
5
|
"openapiPath": "contracts/developer/openapi.yaml",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "eac6c797d7f4a63ed2bdcd3d9bc0b0a0f5b4b889c6c0b2f731ac0738f7c0eb69"
|
|
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.33",
|
|
11
|
+
"integrity": "sha512-sYj/2OEqse7EkVvy9pPRlgocNuN46LVHVtB1kg4ro+56QNMML132kn6to8B3lUoO2SBRwG/u1gN45nTjaetWlw==",
|
|
12
|
+
"operationsSha256": "dc65744bdac51c47863b72536ae7ac7914a72d882f58f8ccc7d1cd3e5a54bec6",
|
|
13
|
+
"workspaceOpenapiSha256": "eac6c797d7f4a63ed2bdcd3d9bc0b0a0f5b4b889c6c0b2f731ac0738f7c0eb69",
|
|
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: component block left as text: ${error}`),
|
|
252
253
|
...(ctx.onPlatformSendDispatch
|
|
253
254
|
? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
|
|
254
255
|
: {}),
|
package/dist/src/dispatch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BUTTONS_GUIDANCE, BUTTONS_BLOCK_INSTRUCTION, PAYMENT_BLOCK_INSTRUCTION, PAYMENT_GUIDANCE, LINK_LINE_INSTRUCTION, SELECTION_GUIDANCE, SELECTION_BLOCK_INSTRUCTION, selectionReplyContext } from "@relaymessenger/sdk";
|
|
1
2
|
import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope, } from "openclaw/plugin-sdk/channel-inbound";
|
|
2
3
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
3
4
|
import { bindIngressLifecycleToReplyOptions } from "openclaw/plugin-sdk/channel-outbound";
|
|
@@ -177,7 +178,9 @@ export async function dispatchRelayEvent(params) {
|
|
|
177
178
|
message: {
|
|
178
179
|
inboundEventKind: "user_request",
|
|
179
180
|
body,
|
|
180
|
-
bodyForAgent: facts.text,
|
|
181
|
+
bodyForAgent: [facts.text, selectionReplyContext(facts.selection, facts.richMessage),
|
|
182
|
+
`${BUTTONS_BLOCK_INSTRUCTION} ${LINK_LINE_INSTRUCTION} ${BUTTONS_GUIDANCE} ${SELECTION_BLOCK_INSTRUCTION} ${SELECTION_GUIDANCE} ${PAYMENT_BLOCK_INSTRUCTION} ${PAYMENT_GUIDANCE}`,
|
|
183
|
+
].filter(Boolean).join("\n\n"),
|
|
181
184
|
rawBody: facts.text,
|
|
182
185
|
commandBody: facts.text,
|
|
183
186
|
},
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectionReply, selectionReplyContext } from "@relaymessenger/sdk";
|
|
1
2
|
function renderPart(part) {
|
|
2
3
|
switch (part.type) {
|
|
3
4
|
case "text":
|
|
@@ -8,12 +9,11 @@ function renderPart(part) {
|
|
|
8
9
|
return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
|
|
9
10
|
case "system":
|
|
10
11
|
return part.value;
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// nothing, again as the server does; its question is the text beside it.
|
|
14
|
-
case "button_reply":
|
|
15
|
-
return part.label;
|
|
12
|
+
// The agent's own buttons part reads as nothing, as on the server; its
|
|
13
|
+
// question is the text beside it. A tap arrives as ordinary text.
|
|
16
14
|
case "buttons":
|
|
15
|
+
case "selection":
|
|
16
|
+
case "selection_response":
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -44,7 +44,9 @@ export function buildRelayInboundFacts(event) {
|
|
|
44
44
|
event.data.sender_handle.kind !== "agent")
|
|
45
45
|
return null;
|
|
46
46
|
const text = renderRelayMessageParts(event.data.parts);
|
|
47
|
-
|
|
47
|
+
const message = { parts: event.data.parts, ...(event.data.reply_to ? { reply_to: event.data.reply_to } : {}) };
|
|
48
|
+
const richMessage = selectionReplyContext(undefined, message) ? message : undefined;
|
|
49
|
+
if (!text.trim() && !richMessage)
|
|
48
50
|
return null;
|
|
49
51
|
const mentionHandles = event.data.parts.flatMap((part) => part.type === "text" &&
|
|
50
52
|
typeof part.mention === "string" &&
|
|
@@ -53,7 +55,10 @@ export function buildRelayInboundFacts(event) {
|
|
|
53
55
|
: []);
|
|
54
56
|
const timestampValue = event.data.sent_at ?? event.created_at;
|
|
55
57
|
const timestamp = Date.parse(timestampValue);
|
|
58
|
+
const selection = selectionReply(event.data.parts, event.data.reply_to);
|
|
56
59
|
return {
|
|
60
|
+
...(selection ? { selection } : {}),
|
|
61
|
+
...(richMessage ? { richMessage } : {}),
|
|
57
62
|
eventId: event.event_id,
|
|
58
63
|
messageId: event.data.id,
|
|
59
64
|
chatId: event.data.chat.id,
|
|
@@ -70,9 +75,11 @@ export function buildRelayInboundFacts(event) {
|
|
|
70
75
|
...(event.data.reply_to?.message_id
|
|
71
76
|
? {
|
|
72
77
|
replyToId: event.data.reply_to.message_id,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
// The answer quotes the person's message, the one it answers (a bot's
|
|
79
|
+
// reply_to in Telegram and Discord names the person's message). A
|
|
80
|
+
// tap's reply_to names the agent's buttons part, which no reply may
|
|
81
|
+
// target, so this is also what keeps a tap answerable.
|
|
82
|
+
replyAnchorId: event.data.id,
|
|
76
83
|
}
|
|
77
84
|
: {}),
|
|
78
85
|
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
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, createPaymentPart, 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,49 @@ 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, payment, error } = answerMessages(params.text);
|
|
31
|
+
if (error)
|
|
32
|
+
params.onButtonsError?.(error);
|
|
33
|
+
if (messages.length === 0 && !payment)
|
|
34
|
+
messages.push([{ type: "text", value: params.text }]);
|
|
35
|
+
if (payment) {
|
|
36
|
+
// Created with the card's own key, so a retry of this delivery returns
|
|
37
|
+
// the same request. A refusal after words went out is reported and the
|
|
38
|
+
// words stand; with nothing sent yet, it is the delivery's own error.
|
|
39
|
+
const key = indexedIdempotencyKey(params.idempotencyKey, messages.length);
|
|
40
|
+
try {
|
|
41
|
+
messages.push([await createPaymentPart(params.relay, payment, key, params.signal ? { signal: params.signal } : undefined)]);
|
|
42
|
+
}
|
|
43
|
+
catch (refusal) {
|
|
44
|
+
if (!(refusal instanceof RelayAPIError) || refusal.retryable || messages.length === 0)
|
|
45
|
+
throw refusal;
|
|
46
|
+
params.onButtonsError?.(`the payment was not sent: ${refusal.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let first;
|
|
50
|
+
for (const [index, parts] of messages.entries()) {
|
|
51
|
+
const response = await params.relay.chats.messages.send(params.chatId, {
|
|
52
|
+
message: {
|
|
53
|
+
parts,
|
|
54
|
+
idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
|
|
55
|
+
...(index === 0 && params.replyToId
|
|
56
|
+
? { reply_to: { message_id: params.replyToId } }
|
|
57
|
+
: {}),
|
|
58
|
+
},
|
|
59
|
+
}, params.signal ? { signal: params.signal } : undefined);
|
|
60
|
+
first ??= response;
|
|
61
|
+
}
|
|
62
|
+
return first;
|
|
31
63
|
}
|
|
32
64
|
export function classifyUnknownRelaySend(error) {
|
|
33
65
|
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.31",
|
|
4
4
|
"description": "Native Relay channel plugin for OpenClaw",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,11 +37,11 @@
|
|
|
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.33"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^26.
|
|
44
|
-
"openclaw": "2026.
|
|
43
|
+
"@types/node": "^26.6.2",
|
|
44
|
+
"openclaw": "2026.9.5",
|
|
45
45
|
"typescript": "^7.0.2",
|
|
46
46
|
"vitest": "^4.1.10"
|
|
47
47
|
},
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"pluginApi": ">=2026.8.1 <2026.10.0"
|
|
165
165
|
},
|
|
166
166
|
"build": {
|
|
167
|
-
"openclawVersion": "2026.
|
|
167
|
+
"openclawVersion": "2026.9.5"
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
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: component block left as text: ${error}`,
|
|
335
|
+
),
|
|
332
336
|
...(ctx.onPlatformSendDispatch
|
|
333
337
|
? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
|
|
334
338
|
: {}),
|
package/src/dispatch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BUTTONS_GUIDANCE, BUTTONS_BLOCK_INSTRUCTION, PAYMENT_BLOCK_INSTRUCTION, PAYMENT_GUIDANCE, LINK_LINE_INSTRUCTION, SELECTION_GUIDANCE, SELECTION_BLOCK_INSTRUCTION, selectionReplyContext } from "@relaymessenger/sdk";
|
|
1
2
|
import type {
|
|
2
3
|
Message,
|
|
3
4
|
Relay,
|
|
@@ -243,7 +244,9 @@ export async function dispatchRelayEvent(params: {
|
|
|
243
244
|
message: {
|
|
244
245
|
inboundEventKind: "user_request",
|
|
245
246
|
body,
|
|
246
|
-
bodyForAgent: facts.text,
|
|
247
|
+
bodyForAgent: [facts.text, selectionReplyContext(facts.selection, facts.richMessage),
|
|
248
|
+
`${BUTTONS_BLOCK_INSTRUCTION} ${LINK_LINE_INSTRUCTION} ${BUTTONS_GUIDANCE} ${SELECTION_BLOCK_INSTRUCTION} ${SELECTION_GUIDANCE} ${PAYMENT_BLOCK_INSTRUCTION} ${PAYMENT_GUIDANCE}`,
|
|
249
|
+
].filter(Boolean).join("\n\n"),
|
|
247
250
|
rawBody: facts.text,
|
|
248
251
|
commandBody: facts.text,
|
|
249
252
|
},
|
package/src/inbound.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectionReply, selectionReplyContext } from "@relaymessenger/sdk";
|
|
1
2
|
import type {
|
|
2
3
|
MessagePartResponse,
|
|
3
4
|
RelayWebhookEvent,
|
|
@@ -17,12 +18,11 @@ function renderPart(part: MessagePartResponse): string | undefined {
|
|
|
17
18
|
return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
|
|
18
19
|
case "system":
|
|
19
20
|
return part.value;
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// nothing, again as the server does; its question is the text beside it.
|
|
23
|
-
case "button_reply":
|
|
24
|
-
return part.label;
|
|
21
|
+
// The agent's own buttons part reads as nothing, as on the server; its
|
|
22
|
+
// question is the text beside it. A tap arrives as ordinary text.
|
|
25
23
|
case "buttons":
|
|
24
|
+
case "selection":
|
|
25
|
+
case "selection_response":
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -64,7 +64,9 @@ export function buildRelayInboundFacts(
|
|
|
64
64
|
) return null;
|
|
65
65
|
|
|
66
66
|
const text = renderRelayMessageParts(event.data.parts);
|
|
67
|
-
|
|
67
|
+
const message = { parts: event.data.parts, ...(event.data.reply_to ? { reply_to: event.data.reply_to } : {}) };
|
|
68
|
+
const richMessage = selectionReplyContext(undefined, message) ? message : undefined;
|
|
69
|
+
if (!text.trim() && !richMessage) return null;
|
|
68
70
|
|
|
69
71
|
const mentionHandles = event.data.parts.flatMap((part) =>
|
|
70
72
|
part.type === "text" &&
|
|
@@ -75,7 +77,10 @@ export function buildRelayInboundFacts(
|
|
|
75
77
|
);
|
|
76
78
|
const timestampValue = event.data.sent_at ?? event.created_at;
|
|
77
79
|
const timestamp = Date.parse(timestampValue);
|
|
80
|
+
const selection = selectionReply(event.data.parts, event.data.reply_to);
|
|
78
81
|
return {
|
|
82
|
+
...(selection ? { selection } : {}),
|
|
83
|
+
...(richMessage ? { richMessage } : {}),
|
|
79
84
|
eventId: event.event_id,
|
|
80
85
|
messageId: event.data.id,
|
|
81
86
|
chatId: event.data.chat.id,
|
|
@@ -93,9 +98,11 @@ export function buildRelayInboundFacts(
|
|
|
93
98
|
...(event.data.reply_to?.message_id
|
|
94
99
|
? {
|
|
95
100
|
replyToId: event.data.reply_to.message_id,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
// The answer quotes the person's message, the one it answers (a bot's
|
|
102
|
+
// reply_to in Telegram and Discord names the person's message). A
|
|
103
|
+
// tap's reply_to names the agent's buttons part, which no reply may
|
|
104
|
+
// target, so this is also what keeps a tap answerable.
|
|
105
|
+
replyAnchorId: event.data.id,
|
|
99
106
|
}
|
|
100
107
|
: {}),
|
|
101
108
|
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
package/src/outbound.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
answerMessages,
|
|
4
|
+
createPaymentPart,
|
|
5
|
+
indexedIdempotencyKey,
|
|
3
6
|
Relay,
|
|
4
7
|
RelayAPIError,
|
|
5
8
|
type MessageSendResponse,
|
|
@@ -32,29 +35,58 @@ export function deriveRelayIdempotencyKey(params: {
|
|
|
32
35
|
: `relay-openclaw:sha256:${createHash("sha256").update(raw).digest("hex")}`;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* OpenClaw hands the agent's words as text, so buttons ride in them as the
|
|
40
|
+
* SDK's fenced block, lifted here into the buttons part, and a link written
|
|
41
|
+
* alone on a line goes out as its own link Message. A block that cannot be
|
|
42
|
+
* read stays in the words and is reported through `onButtonsError`. Without
|
|
43
|
+
* a block or a link line the words go exactly as OpenClaw handed them, in one
|
|
44
|
+
* Message; the response is the first Message's, the one the reply anchors to.
|
|
45
|
+
*/
|
|
35
46
|
export async function sendRelayText(params: {
|
|
36
|
-
relay: Pick<Relay, "chats">;
|
|
47
|
+
relay: Pick<Relay, "chats" | "paymentRequests">;
|
|
37
48
|
chatId: string;
|
|
38
49
|
text: string;
|
|
39
50
|
replyToId?: string | null | undefined;
|
|
40
51
|
idempotencyKey: string;
|
|
41
52
|
signal?: AbortSignal;
|
|
42
53
|
onPlatformSendDispatch?: () => Promise<void>;
|
|
54
|
+
onButtonsError?: (error: string) => void;
|
|
43
55
|
}): Promise<MessageSendResponse> {
|
|
44
56
|
await params.onPlatformSendDispatch?.();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const { messages, payment, error } = answerMessages(params.text);
|
|
58
|
+
if (error) params.onButtonsError?.(error);
|
|
59
|
+
if (messages.length === 0 && !payment) messages.push([{ type: "text", value: params.text }]);
|
|
60
|
+
if (payment) {
|
|
61
|
+
// Created with the card's own key, so a retry of this delivery returns
|
|
62
|
+
// the same request. A refusal after words went out is reported and the
|
|
63
|
+
// words stand; with nothing sent yet, it is the delivery's own error.
|
|
64
|
+
const key = indexedIdempotencyKey(params.idempotencyKey, messages.length);
|
|
65
|
+
try {
|
|
66
|
+
messages.push([await createPaymentPart(params.relay, payment, key, params.signal ? { signal: params.signal } : undefined)]);
|
|
67
|
+
} catch (refusal) {
|
|
68
|
+
if (!(refusal instanceof RelayAPIError) || refusal.retryable || messages.length === 0) throw refusal;
|
|
69
|
+
params.onButtonsError?.(`the payment was not sent: ${refusal.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let first: MessageSendResponse | undefined;
|
|
73
|
+
for (const [index, parts] of messages.entries()) {
|
|
74
|
+
const response = await params.relay.chats.messages.send(
|
|
75
|
+
params.chatId,
|
|
76
|
+
{
|
|
77
|
+
message: {
|
|
78
|
+
parts,
|
|
79
|
+
idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
|
|
80
|
+
...(index === 0 && params.replyToId
|
|
81
|
+
? { reply_to: { message_id: params.replyToId } }
|
|
82
|
+
: {}),
|
|
83
|
+
},
|
|
54
84
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
params.signal ? { signal: params.signal } : undefined,
|
|
86
|
+
);
|
|
87
|
+
first ??= response;
|
|
88
|
+
}
|
|
89
|
+
return first!;
|
|
58
90
|
}
|
|
59
91
|
|
|
60
92
|
export function classifyUnknownRelaySend(error: unknown): {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Chat,
|
|
3
|
+
SelectionReply,
|
|
4
|
+
MessagePartResponse,
|
|
5
|
+
ReplyTo,
|
|
3
6
|
ChatHandle,
|
|
4
7
|
MessageWebhookData,
|
|
5
8
|
Message,
|
|
@@ -61,6 +64,8 @@ export type RelayMessageReceivedEvent = RelayWebhookEnvelope<
|
|
|
61
64
|
>;
|
|
62
65
|
|
|
63
66
|
export type RelayInboundFacts = {
|
|
67
|
+
selection?: SelectionReply;
|
|
68
|
+
richMessage?: { parts: MessagePartResponse[]; reply_to?: ReplyTo | null };
|
|
64
69
|
eventId: string;
|
|
65
70
|
messageId: string;
|
|
66
71
|
chatId: string;
|
|
@@ -73,10 +78,9 @@ export type RelayInboundFacts = {
|
|
|
73
78
|
ownerHandle?: ChatHandle;
|
|
74
79
|
replyToId?: string;
|
|
75
80
|
/**
|
|
76
|
-
* The Message an outbound reply should quote
|
|
77
|
-
*
|
|
78
|
-
* agent's
|
|
79
|
-
* the person quoted.
|
|
81
|
+
* The Message an outbound reply should quote when the person's Message
|
|
82
|
+
* was itself a reply: the person's Message. A tap's reply_to names the
|
|
83
|
+
* agent's buttons part, which no reply may target.
|
|
80
84
|
*/
|
|
81
85
|
replyAnchorId?: string;
|
|
82
86
|
timestamp?: number;
|