@relaymessenger/openclaw-plugin 0.4.7-staging.39 → 0.4.7-staging.40
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 +5 -5
- package/dist/src/dispatch.js +53 -2
- package/dist/src/inbound.js +3 -0
- package/package.json +2 -2
- package/src/dispatch.ts +63 -4
- package/src/inbound.ts +3 -0
- package/src/types.ts +2 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"relayServer": {
|
|
3
3
|
"repository": "RelayMessenger/Relay-Server",
|
|
4
|
-
"commit": "
|
|
4
|
+
"commit": "65c4473526e4bbd3fedcd747ab09d218d00920ce",
|
|
5
5
|
"openapiPath": "contracts/developer/openapi.yaml",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "44d3202de07206707c8914c37029abc365c144be4245e677574210d2a1cf6f41"
|
|
7
7
|
},
|
|
8
8
|
"relaySdk": {
|
|
9
9
|
"package": "@relaymessenger/sdk",
|
|
10
|
-
"version": "0.3.6-staging.
|
|
11
|
-
"integrity": "sha512-
|
|
10
|
+
"version": "0.3.6-staging.47",
|
|
11
|
+
"integrity": "sha512-CI67lgtiuYyhSwuttqXF6x07OC9fDwTIpT3eCZPiqHGxWDo6Tx/zNmAAUzEDcw9ExoGOHP2VXnIlcDFMlQrslw==",
|
|
12
12
|
"operationsSha256": "3e09d345c249709d942d2efb89ed7e84934c19552767d8f4438eb086dd917e1c",
|
|
13
|
-
"workspaceOpenapiSha256": "
|
|
13
|
+
"workspaceOpenapiSha256": "44d3202de07206707c8914c37029abc365c144be4245e677574210d2a1cf6f41",
|
|
14
14
|
"usedOperations": [
|
|
15
15
|
{
|
|
16
16
|
"method": "GET",
|
package/dist/src/dispatch.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BUTTONS_GUIDANCE, BUTTONS_BLOCK_INSTRUCTION, PAYMENT_BLOCK_INSTRUCTION,
|
|
|
2
2
|
import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope, } from "openclaw/plugin-sdk/channel-inbound";
|
|
3
3
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
4
4
|
import { bindIngressLifecycleToReplyOptions } from "openclaw/plugin-sdk/channel-outbound";
|
|
5
|
-
import { buildRelayInboundFacts } from "./inbound.js";
|
|
5
|
+
import { buildRelayInboundFacts, renderRelayMessageParts } from "./inbound.js";
|
|
6
6
|
import { waitForIdleChat } from "./turns.js";
|
|
7
7
|
function isReplyToAgentMessage(message, chatId) {
|
|
8
8
|
return message.chat_id === chatId && message.is_from_me === true;
|
|
@@ -30,7 +30,8 @@ export async function resolveRelayTurnActivation(params) {
|
|
|
30
30
|
}
|
|
31
31
|
if (!params.facts.replyToId)
|
|
32
32
|
return null;
|
|
33
|
-
const replyTarget =
|
|
33
|
+
const replyTarget = params.replyTarget
|
|
34
|
+
?? await params.relay.messages.retrieve(params.facts.replyToId);
|
|
34
35
|
if (!isReplyToAgentMessage(replyTarget, params.facts.chatId))
|
|
35
36
|
return null;
|
|
36
37
|
return {
|
|
@@ -39,6 +40,48 @@ export async function resolveRelayTurnActivation(params) {
|
|
|
39
40
|
implicitMentionKinds: ["reply_to_bot"],
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The Message a swipe-reply answers, read once for the quote and for group
|
|
45
|
+
* activation. A failed read is logged and the turn runs without the quote;
|
|
46
|
+
* group activation then reads it itself and fails the delivery as before.
|
|
47
|
+
*/
|
|
48
|
+
async function readReplyTarget(params) {
|
|
49
|
+
if (!params.facts.replyToId)
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
return await params.relay.messages.retrieve(params.facts.replyToId);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
params.warn?.(`relay: could not read the Message ${params.facts.replyToId} that ${params.facts.messageId} replies to: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* OpenClaw's own reply context, `supplemental.quote`, which it renders to the
|
|
61
|
+
* model as "Reply target of current user message" (id, sender, body), as its
|
|
62
|
+
* Telegram channel fills it from Telegram's `reply_to_message`. A reply names
|
|
63
|
+
* one bubble: when the target has more than one part, only the swiped part is
|
|
64
|
+
* quoted, the rule Relay's iOS app uses to draw the quote.
|
|
65
|
+
*/
|
|
66
|
+
export function relayReplyQuote(facts, target) {
|
|
67
|
+
if (!target || target.chat_id !== facts.chatId)
|
|
68
|
+
return undefined;
|
|
69
|
+
const parts = target.parts ?? [];
|
|
70
|
+
const swiped = parts.length > 1 && facts.replyToPartIndex !== undefined
|
|
71
|
+
? parts[facts.replyToPartIndex]
|
|
72
|
+
: undefined;
|
|
73
|
+
const body = renderRelayMessageParts(swiped ? [swiped] : parts);
|
|
74
|
+
const sender = target.from_handle?.display_name?.trim()
|
|
75
|
+
|| target.from_handle?.handle
|
|
76
|
+
|| target.from
|
|
77
|
+
|| undefined;
|
|
78
|
+
return {
|
|
79
|
+
id: target.id,
|
|
80
|
+
...(body ? { body } : {}),
|
|
81
|
+
...(sender ? { sender } : {}),
|
|
82
|
+
senderAllowed: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
42
85
|
/**
|
|
43
86
|
* The answer to another agent names the Message it answers: the model's own
|
|
44
87
|
* reply target when it chose one, else the agent's Message. Where no reply may
|
|
@@ -60,9 +103,15 @@ export async function dispatchRelayEvent(params) {
|
|
|
60
103
|
params.warn?.(`relay: durably accepted ${params.event.event_type} event ${params.event.event_id} without an agent turn`);
|
|
61
104
|
return;
|
|
62
105
|
}
|
|
106
|
+
const repliedTo = await readReplyTarget({
|
|
107
|
+
facts,
|
|
108
|
+
relay: params.relay,
|
|
109
|
+
warn: params.warn,
|
|
110
|
+
});
|
|
63
111
|
const activation = await resolveRelayTurnActivation({
|
|
64
112
|
facts,
|
|
65
113
|
relay: params.relay,
|
|
114
|
+
...(repliedTo ? { replyTarget: repliedTo } : {}),
|
|
66
115
|
});
|
|
67
116
|
if (!activation) {
|
|
68
117
|
params.warn?.(`relay: durably accepted unmentioned group Message ${facts.messageId} without an agent turn`);
|
|
@@ -164,6 +213,7 @@ export async function dispatchRelayEvent(params) {
|
|
|
164
213
|
...(facts.timestamp ? { timestamp: facts.timestamp } : {}),
|
|
165
214
|
body: facts.text,
|
|
166
215
|
});
|
|
216
|
+
const quote = relayReplyQuote(facts, repliedTo);
|
|
167
217
|
const ctxPayload = buildChannelInboundEventContext({
|
|
168
218
|
channel: "relay",
|
|
169
219
|
accountId: route.accountId ?? params.account.accountId,
|
|
@@ -203,6 +253,7 @@ export async function dispatchRelayEvent(params) {
|
|
|
203
253
|
rawBody: facts.text,
|
|
204
254
|
commandBody: facts.text,
|
|
205
255
|
},
|
|
256
|
+
...(quote ? { supplemental: { quote } } : {}),
|
|
206
257
|
channelIngress: access,
|
|
207
258
|
access: {
|
|
208
259
|
commands: {
|
package/dist/src/inbound.js
CHANGED
|
@@ -88,6 +88,9 @@ export function buildRelayInboundFacts(event) {
|
|
|
88
88
|
...(event.data.reply_to?.message_id
|
|
89
89
|
? {
|
|
90
90
|
replyToId: event.data.reply_to.message_id,
|
|
91
|
+
...(event.data.reply_to.part_index === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { replyToPartIndex: event.data.reply_to.part_index }),
|
|
91
94
|
// The answer quotes the person's message, the one it answers (a bot's
|
|
92
95
|
// reply_to in Telegram and Discord names the person's message). A
|
|
93
96
|
// tap's reply_to names the agent's buttons part, which no reply may
|
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.40",
|
|
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.47"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.6.2",
|
package/src/dispatch.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-
|
|
|
12
12
|
import { bindIngressLifecycleToReplyOptions } from "openclaw/plugin-sdk/channel-outbound";
|
|
13
13
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
14
14
|
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
|
15
|
-
import { buildRelayInboundFacts } from "./inbound.js";
|
|
15
|
+
import { buildRelayInboundFacts, renderRelayMessageParts } from "./inbound.js";
|
|
16
16
|
import type { RelayIngressLifecycle } from "./ingress.js";
|
|
17
17
|
import type { PluginRuntime } from "./runtime.js";
|
|
18
18
|
import { type RelayChatTurns, waitForIdleChat } from "./turns.js";
|
|
@@ -55,6 +55,8 @@ function isReplyToAgentMessage(
|
|
|
55
55
|
export async function resolveRelayTurnActivation(params: {
|
|
56
56
|
facts: RelayInboundFacts;
|
|
57
57
|
relay: RelayReplyLookup;
|
|
58
|
+
/** The replied-to Message when the caller already read it. */
|
|
59
|
+
replyTarget?: Message;
|
|
58
60
|
}): Promise<RelayTurnActivation | null> {
|
|
59
61
|
if (params.facts.chatType === "direct") {
|
|
60
62
|
return {
|
|
@@ -77,9 +79,8 @@ export async function resolveRelayTurnActivation(params: {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
if (!params.facts.replyToId) return null;
|
|
80
|
-
const replyTarget =
|
|
81
|
-
params.facts.replyToId
|
|
82
|
-
);
|
|
82
|
+
const replyTarget = params.replyTarget
|
|
83
|
+
?? await params.relay.messages.retrieve(params.facts.replyToId);
|
|
83
84
|
if (!isReplyToAgentMessage(replyTarget, params.facts.chatId)) return null;
|
|
84
85
|
return {
|
|
85
86
|
kind: "reply",
|
|
@@ -88,6 +89,56 @@ export async function resolveRelayTurnActivation(params: {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The Message a swipe-reply answers, read once for the quote and for group
|
|
94
|
+
* activation. A failed read is logged and the turn runs without the quote;
|
|
95
|
+
* group activation then reads it itself and fails the delivery as before.
|
|
96
|
+
*/
|
|
97
|
+
async function readReplyTarget(params: {
|
|
98
|
+
facts: RelayInboundFacts;
|
|
99
|
+
relay: RelayReplyLookup;
|
|
100
|
+
warn: ((message: string) => void) | undefined;
|
|
101
|
+
}): Promise<Message | undefined> {
|
|
102
|
+
if (!params.facts.replyToId) return undefined;
|
|
103
|
+
try {
|
|
104
|
+
return await params.relay.messages.retrieve(params.facts.replyToId);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
params.warn?.(
|
|
107
|
+
`relay: could not read the Message ${params.facts.replyToId} that ${params.facts.messageId} replies to: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* OpenClaw's own reply context, `supplemental.quote`, which it renders to the
|
|
115
|
+
* model as "Reply target of current user message" (id, sender, body), as its
|
|
116
|
+
* Telegram channel fills it from Telegram's `reply_to_message`. A reply names
|
|
117
|
+
* one bubble: when the target has more than one part, only the swiped part is
|
|
118
|
+
* quoted, the rule Relay's iOS app uses to draw the quote.
|
|
119
|
+
*/
|
|
120
|
+
export function relayReplyQuote(
|
|
121
|
+
facts: Pick<RelayInboundFacts, "chatId" | "replyToPartIndex">,
|
|
122
|
+
target: Message | undefined,
|
|
123
|
+
) {
|
|
124
|
+
if (!target || target.chat_id !== facts.chatId) return undefined;
|
|
125
|
+
const parts = target.parts ?? [];
|
|
126
|
+
const swiped = parts.length > 1 && facts.replyToPartIndex !== undefined
|
|
127
|
+
? parts[facts.replyToPartIndex]
|
|
128
|
+
: undefined;
|
|
129
|
+
const body = renderRelayMessageParts(swiped ? [swiped] : parts);
|
|
130
|
+
const sender = target.from_handle?.display_name?.trim()
|
|
131
|
+
|| target.from_handle?.handle
|
|
132
|
+
|| target.from
|
|
133
|
+
|| undefined;
|
|
134
|
+
return {
|
|
135
|
+
id: target.id,
|
|
136
|
+
...(body ? { body } : {}),
|
|
137
|
+
...(sender ? { sender } : {}),
|
|
138
|
+
senderAllowed: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
91
142
|
/**
|
|
92
143
|
* The answer to another agent names the Message it answers: the model's own
|
|
93
144
|
* reply target when it chose one, else the agent's Message. Where no reply may
|
|
@@ -124,9 +175,15 @@ export async function dispatchRelayEvent(params: {
|
|
|
124
175
|
return;
|
|
125
176
|
}
|
|
126
177
|
|
|
178
|
+
const repliedTo = await readReplyTarget({
|
|
179
|
+
facts,
|
|
180
|
+
relay: params.relay,
|
|
181
|
+
warn: params.warn,
|
|
182
|
+
});
|
|
127
183
|
const activation = await resolveRelayTurnActivation({
|
|
128
184
|
facts,
|
|
129
185
|
relay: params.relay,
|
|
186
|
+
...(repliedTo ? { replyTarget: repliedTo } : {}),
|
|
130
187
|
});
|
|
131
188
|
if (!activation) {
|
|
132
189
|
params.warn?.(
|
|
@@ -235,6 +292,7 @@ export async function dispatchRelayEvent(params: {
|
|
|
235
292
|
...(facts.timestamp ? { timestamp: facts.timestamp } : {}),
|
|
236
293
|
body: facts.text,
|
|
237
294
|
});
|
|
295
|
+
const quote = relayReplyQuote(facts, repliedTo);
|
|
238
296
|
const ctxPayload = buildChannelInboundEventContext({
|
|
239
297
|
channel: "relay",
|
|
240
298
|
accountId: route.accountId ?? params.account.accountId,
|
|
@@ -274,6 +332,7 @@ export async function dispatchRelayEvent(params: {
|
|
|
274
332
|
rawBody: facts.text,
|
|
275
333
|
commandBody: facts.text,
|
|
276
334
|
},
|
|
335
|
+
...(quote ? { supplemental: { quote } } : {}),
|
|
277
336
|
channelIngress: access,
|
|
278
337
|
access: {
|
|
279
338
|
commands: {
|
package/src/inbound.ts
CHANGED
|
@@ -111,6 +111,9 @@ export function buildRelayInboundFacts(
|
|
|
111
111
|
...(event.data.reply_to?.message_id
|
|
112
112
|
? {
|
|
113
113
|
replyToId: event.data.reply_to.message_id,
|
|
114
|
+
...(event.data.reply_to.part_index === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { replyToPartIndex: event.data.reply_to.part_index }),
|
|
114
117
|
// The answer quotes the person's message, the one it answers (a bot's
|
|
115
118
|
// reply_to in Telegram and Discord names the person's message). A
|
|
116
119
|
// tap's reply_to names the agent's buttons part, which no reply may
|
package/src/types.ts
CHANGED
|
@@ -77,6 +77,8 @@ export type RelayInboundFacts = {
|
|
|
77
77
|
mentionHandles: string[];
|
|
78
78
|
ownerHandle?: ChatHandle;
|
|
79
79
|
replyToId?: string;
|
|
80
|
+
/** The part of the replied-to Message the person swiped (`reply_to.part_index`). */
|
|
81
|
+
replyToPartIndex?: number;
|
|
80
82
|
/**
|
|
81
83
|
* The Message an outbound reply should quote when the person's Message
|
|
82
84
|
* was itself a reply: the person's Message. A tap's reply_to names the
|