@newbase-clawchat/openclaw-clawchat 2026.5.4-1 → 2026.5.4-2
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 +34 -1
- package/dist/src/channel.js +2 -1
- package/dist/src/client.js +11 -4
- package/dist/src/media-runtime.js +6 -5
- package/dist/src/outbound.js +12 -2
- package/dist/src/reply-dispatcher.js +4 -9
- package/dist/src/runtime.js +6 -6
- package/package.json +1 -1
- package/src/channel.outbound.test.ts +6 -0
- package/src/channel.test.ts +22 -0
- package/src/channel.ts +1 -0
- package/src/client.test.ts +63 -2
- package/src/client.ts +12 -3
- package/src/manifest.test.ts +14 -24
- package/src/media-runtime.test.ts +26 -0
- package/src/media-runtime.ts +19 -7
- package/src/outbound.test.ts +1 -1
- package/src/outbound.ts +11 -2
- package/src/reply-dispatcher.test.ts +100 -2
- package/src/reply-dispatcher.ts +4 -9
- package/src/runtime.test.ts +2 -0
- package/src/runtime.ts +6 -6
- package/src/scripts.test.ts +42 -0
package/README.md
CHANGED
|
@@ -21,7 +21,8 @@ npm i @newbase-clawchat/openclaw-clawchat
|
|
|
21
21
|
|
|
22
22
|
Requires `openclaw >= 2026.5.4` as a peer host.
|
|
23
23
|
|
|
24
|
-
For the OpenClaw plugin install/update flow,
|
|
24
|
+
For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
|
|
25
|
+
command documented in [`INSTALL.md`](./INSTALL.md).
|
|
25
26
|
|
|
26
27
|
Example LLM prompt:
|
|
27
28
|
|
|
@@ -176,6 +177,38 @@ npm run dev:openclaw-source
|
|
|
176
177
|
This checkout is local-only. It is ignored by git and is not required to run the
|
|
177
178
|
plugin tests or publish the package.
|
|
178
179
|
|
|
180
|
+
## R2 package scripts
|
|
181
|
+
|
|
182
|
+
Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
./package_openclaw_plugin.sh
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The script runs `npm pack`, uploads the generated `.tgz` to the configured R2
|
|
189
|
+
bucket, and prints the public URL. R2 credentials are read from `.env.r2`, which
|
|
190
|
+
is ignored by git. Use `--no-upload` to build the tarball without uploading it.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
AWS_ACCESS_KEY_ID=...
|
|
194
|
+
AWS_SECRET_ACCESS_KEY=...
|
|
195
|
+
AWS_DEFAULT_REGION=auto
|
|
196
|
+
R2_ENDPOINT=https://...
|
|
197
|
+
R2_BUCKET=...
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Install the R2-hosted tarball on a device or container with OpenClaw available:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
./install_openclaw.sh
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To install a specific uploaded tarball, pass its URL explicitly:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
./install_openclaw.sh https://dddddddddddddtest.clawling.chat/openclaw/newbase-clawchat-openclaw-clawchat-2026.5.4-2.tgz
|
|
210
|
+
```
|
|
211
|
+
|
|
179
212
|
## License
|
|
180
213
|
|
|
181
214
|
See the repository root.
|
package/dist/src/channel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
3
|
-
import { resolveOpenclawClawlingAccount, } from "./config.js";
|
|
3
|
+
import { CHANNEL_ID, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
4
4
|
import { openclawClawlingOutbound } from "./outbound.js";
|
|
5
5
|
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
|
|
6
6
|
import { openclawClawlingSetupPlugin } from "./channel.setup.js";
|
|
@@ -53,6 +53,7 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
|
53
53
|
],
|
|
54
54
|
},
|
|
55
55
|
messaging: {
|
|
56
|
+
targetPrefixes: ["cc", "clawchat", CHANNEL_ID],
|
|
56
57
|
normalizeTarget: (target) => target
|
|
57
58
|
.trim()
|
|
58
59
|
.replace(/^openclaw-clawchat:/i, "")
|
package/dist/src/client.js
CHANGED
|
@@ -44,12 +44,19 @@ function normalizeRouting(params) {
|
|
|
44
44
|
* Emit a raw v2 envelope directly over the transport so we can carry top-level
|
|
45
45
|
* `chat_id` routing without SDK-injected `to` metadata.
|
|
46
46
|
*/
|
|
47
|
-
function emitEnvelope(client, event, payload, routing) {
|
|
47
|
+
function emitEnvelope(client, event, payload, routing, options = {}) {
|
|
48
48
|
const inner = client;
|
|
49
|
-
if (!inner.
|
|
50
|
-
inner.emitRaw
|
|
49
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
50
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
+
if (!inner.opts?.transport) {
|
|
54
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
55
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
|
|
59
|
+
}
|
|
53
60
|
const env = {
|
|
54
61
|
version: "2",
|
|
55
62
|
event,
|
|
@@ -150,7 +157,7 @@ export function emitFinalStreamReply(client, params) {
|
|
|
150
157
|
},
|
|
151
158
|
},
|
|
152
159
|
},
|
|
153
|
-
}, routing);
|
|
160
|
+
}, routing, { forceRawTransport: true });
|
|
154
161
|
}
|
|
155
162
|
export function emitStreamFailed(client, params) {
|
|
156
163
|
const now = Date.now();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildOutboundMediaLoadOptions, } from "openclaw/plugin-sdk/media-runtime";
|
|
1
2
|
export function inferMediaKindFromMime(mime) {
|
|
2
3
|
if (!mime)
|
|
3
4
|
return "file";
|
|
@@ -56,12 +57,12 @@ export async function uploadOutboundMedia(urls, ctx) {
|
|
|
56
57
|
const out = [];
|
|
57
58
|
for (const url of urls) {
|
|
58
59
|
try {
|
|
59
|
-
const loaded = await ctx.runtime.media.loadWebMedia(url, {
|
|
60
|
+
const loaded = await ctx.runtime.media.loadWebMedia(url, buildOutboundMediaLoadOptions({
|
|
60
61
|
maxBytes,
|
|
61
|
-
...(ctx.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
62
|
+
...(ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {}),
|
|
63
|
+
...(ctx.mediaLocalRoots ? { mediaLocalRoots: ctx.mediaLocalRoots } : {}),
|
|
64
|
+
...(ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}),
|
|
65
|
+
}));
|
|
65
66
|
const uploaded = await ctx.apiClient.uploadMedia({
|
|
66
67
|
buffer: loaded.buffer,
|
|
67
68
|
filename: loaded.fileName ?? "upload.bin",
|
package/dist/src/outbound.js
CHANGED
|
@@ -19,6 +19,8 @@ import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawC
|
|
|
19
19
|
* - `clawchat:group:{chat_id}` → group
|
|
20
20
|
* - `openclaw-clawchat:direct:{chat_id}` → direct
|
|
21
21
|
* - `openclaw-clawchat:group:{chat_id}` → group
|
|
22
|
+
* - `direct:{chat_id}` → direct (host-normalized)
|
|
23
|
+
* - `group:{chat_id}` → group (host-normalized)
|
|
22
24
|
* - bare `{chat_id}` → direct (backward compat)
|
|
23
25
|
*/
|
|
24
26
|
export function parseOpenclawRecipient(to) {
|
|
@@ -30,6 +32,12 @@ export function parseOpenclawRecipient(to) {
|
|
|
30
32
|
return { chatId: raw, chatType: "direct" };
|
|
31
33
|
const scheme = raw.slice(0, firstColon).toLowerCase();
|
|
32
34
|
const rest = raw.slice(firstColon + 1);
|
|
35
|
+
if (scheme === "direct" || scheme === "group") {
|
|
36
|
+
const chatId = rest.trim();
|
|
37
|
+
if (!chatId)
|
|
38
|
+
throw new Error(`openclaw-clawchat: missing chat_id in "${to}"`);
|
|
39
|
+
return { chatId, chatType: scheme };
|
|
40
|
+
}
|
|
33
41
|
if (scheme !== "cc" && scheme !== "clawchat" && scheme !== CHANNEL_ID) {
|
|
34
42
|
return { chatId: raw, chatType: "direct" };
|
|
35
43
|
}
|
|
@@ -74,7 +82,7 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
74
82
|
mode: "normal",
|
|
75
83
|
replyTo: {
|
|
76
84
|
msgId: params.replyCtx.replyToMessageId,
|
|
77
|
-
senderId: params.replyCtx.
|
|
85
|
+
senderId: params.replyCtx.replyPreviewSenderId,
|
|
78
86
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
79
87
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
80
88
|
},
|
|
@@ -144,7 +152,7 @@ export const openclawClawlingOutbound = {
|
|
|
144
152
|
messageId: result?.messageId ?? `${account.userId}-${Date.now()}`,
|
|
145
153
|
};
|
|
146
154
|
},
|
|
147
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots }) => {
|
|
155
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
148
156
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
149
157
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
150
158
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
@@ -160,7 +168,9 @@ export const openclawClawlingOutbound = {
|
|
|
160
168
|
const mediaFragments = await uploadOutboundMedia([mediaUrl.trim()], {
|
|
161
169
|
apiClient,
|
|
162
170
|
runtime,
|
|
171
|
+
...(mediaAccess ? { mediaAccess } : {}),
|
|
163
172
|
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
|
|
173
|
+
...(mediaReadFile ? { mediaReadFile } : {}),
|
|
164
174
|
});
|
|
165
175
|
if (mediaFragments.length === 0) {
|
|
166
176
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
|
|
2
|
+
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
2
3
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
4
|
import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-stream.js";
|
|
4
5
|
import { emitFinalStreamReply } from "./client.js";
|
|
@@ -256,7 +257,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
256
257
|
routing,
|
|
257
258
|
replyTo: {
|
|
258
259
|
msgId: inboundMessageId ?? streamingMessageId,
|
|
259
|
-
previewId: inboundForFinalReply?.
|
|
260
|
+
previewId: inboundForFinalReply?.senderId ?? target.chatId,
|
|
260
261
|
nickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
|
|
261
262
|
fragments: inboundForFinalReply?.bodyText
|
|
262
263
|
? [{ kind: "text", text: inboundForFinalReply.bodyText }]
|
|
@@ -271,10 +272,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
271
272
|
}
|
|
272
273
|
if (text)
|
|
273
274
|
streamText = mergeStreamingText(streamText, text);
|
|
274
|
-
const urls =
|
|
275
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
276
|
-
...(payload.mediaUrls ?? []),
|
|
277
|
-
].filter((u) => Boolean(u));
|
|
275
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
278
276
|
for (const url of urls) {
|
|
279
277
|
if (!accumulatedMediaUrls.includes(url))
|
|
280
278
|
accumulatedMediaUrls.push(url);
|
|
@@ -313,10 +311,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
313
311
|
deliver: async (payload, info) => {
|
|
314
312
|
const richFragment = buildRichInteractionFragment(payload);
|
|
315
313
|
const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
|
|
316
|
-
const urls =
|
|
317
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
318
|
-
...(payload.mediaUrls ?? []),
|
|
319
|
-
].filter((u) => Boolean(u));
|
|
314
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
320
315
|
log?.info?.(`[${account.accountId}] openclaw-clawchat deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
|
|
321
316
|
if (payload.isReasoning) {
|
|
322
317
|
if (!account.forwardThinking)
|
package/dist/src/runtime.js
CHANGED
|
@@ -140,16 +140,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
140
140
|
timestamp: turn.timestamp,
|
|
141
141
|
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
142
142
|
});
|
|
143
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
143
144
|
const ctxPayload = rt.reply.finalizeInboundContext({
|
|
144
145
|
Body: body,
|
|
145
146
|
BodyForAgent: turn.rawBody,
|
|
146
147
|
RawBody: turn.rawBody,
|
|
147
148
|
CommandBody: turn.rawBody,
|
|
148
|
-
// Clawling v2 routes by chat_id. `
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
|
|
149
|
+
// Clawling v2 routes by chat_id. `OriginatingTo` is what the
|
|
150
|
+
// message tool uses as the implicit current-chat target, so keep it
|
|
151
|
+
// on the conversation id rather than the agent account user id.
|
|
152
|
+
From: conversationTarget,
|
|
153
153
|
To: `${CHANNEL_ID}:${account.userId}`,
|
|
154
154
|
SessionKey: route.sessionKey,
|
|
155
155
|
AccountId: route.accountId ?? accountId,
|
|
@@ -162,7 +162,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
162
162
|
MessageSidFull: turn.messageId,
|
|
163
163
|
Timestamp: turn.timestamp,
|
|
164
164
|
OriginatingChannel: CHANNEL_ID,
|
|
165
|
-
OriginatingTo:
|
|
165
|
+
OriginatingTo: conversationTarget,
|
|
166
166
|
});
|
|
167
167
|
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
168
168
|
const inboundPaths = turn.mediaItems.length > 0
|
package/package.json
CHANGED
|
@@ -88,6 +88,8 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
88
88
|
uploadOutboundMediaMock.mockResolvedValue([
|
|
89
89
|
{ kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
|
|
90
90
|
]);
|
|
91
|
+
const mediaReadFile = vi.fn(async () => Buffer.from("host-read"));
|
|
92
|
+
const mediaAccess = { localRoots: ["/tmp"], workspaceDir: "/workspace" };
|
|
91
93
|
|
|
92
94
|
const { openclawClawlingOutbound } = await import("./outbound.ts");
|
|
93
95
|
const result = await openclawClawlingOutbound.sendMedia!({
|
|
@@ -105,7 +107,9 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
105
107
|
to: "cc:group:room-1",
|
|
106
108
|
text: "caption",
|
|
107
109
|
mediaUrl: "/tmp/photo.png",
|
|
110
|
+
mediaAccess,
|
|
108
111
|
mediaLocalRoots: ["/tmp"],
|
|
112
|
+
mediaReadFile,
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
expect(createApiClientMock).toHaveBeenCalledWith({
|
|
@@ -116,7 +120,9 @@ describe("openclaw-clawchat channel outbound", () => {
|
|
|
116
120
|
expect(uploadOutboundMediaMock).toHaveBeenCalledWith(["/tmp/photo.png"], {
|
|
117
121
|
apiClient,
|
|
118
122
|
runtime,
|
|
123
|
+
mediaAccess,
|
|
119
124
|
mediaLocalRoots: ["/tmp"],
|
|
125
|
+
mediaReadFile,
|
|
120
126
|
});
|
|
121
127
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
122
128
|
expect.objectContaining({
|
package/src/channel.test.ts
CHANGED
|
@@ -123,10 +123,32 @@ describe("openclaw-clawchat plugin", () => {
|
|
|
123
123
|
expect(normalized).toBe("usr_01KPN6SQFQEGM9HR11CHRHPMMT");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it("declares ClawChat target prefixes for OpenClaw channel selection", () => {
|
|
127
|
+
expect(openclawClawlingPlugin.messaging?.targetPrefixes).toEqual([
|
|
128
|
+
"cc",
|
|
129
|
+
"clawchat",
|
|
130
|
+
"openclaw-clawchat",
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
126
134
|
it("parses openclaw-clawchat target prefix as a direct recipient", () => {
|
|
127
135
|
expect(parseOpenclawRecipient("openclaw-clawchat:usr_01KPN6SQFQEGM9HR11CHRHPMMT")).toEqual({
|
|
128
136
|
chatId: "usr_01KPN6SQFQEGM9HR11CHRHPMMT",
|
|
129
137
|
chatType: "direct",
|
|
130
138
|
});
|
|
131
139
|
});
|
|
140
|
+
|
|
141
|
+
it("parses host-normalized group targets as group recipients", () => {
|
|
142
|
+
expect(parseOpenclawRecipient("group:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
|
|
143
|
+
chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
|
|
144
|
+
chatType: "group",
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("parses host-normalized direct targets as direct recipients", () => {
|
|
149
|
+
expect(parseOpenclawRecipient("direct:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
|
|
150
|
+
chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
|
|
151
|
+
chatType: "direct",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
132
154
|
});
|
package/src/channel.ts
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { MockTransport } from "@newbase-clawchat/sdk";
|
|
1
|
+
import { MockTransport, type ClawlingChatClient } from "@newbase-clawchat/sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
createOpenclawClawlingClient,
|
|
6
6
|
emitStreamCreated,
|
|
7
7
|
emitStreamAdd,
|
|
8
8
|
emitStreamDone,
|
|
9
9
|
emitStreamFailed,
|
|
10
|
+
emitFinalStreamReply,
|
|
10
11
|
} from "./client.ts";
|
|
11
12
|
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
12
13
|
|
|
@@ -100,6 +101,66 @@ describe("openclaw-clawchat client", () => {
|
|
|
100
101
|
client.close();
|
|
101
102
|
});
|
|
102
103
|
|
|
104
|
+
it("uses SDK emitRaw with chat_id routing for stream lifecycle frames", () => {
|
|
105
|
+
const transportSend = vi.fn();
|
|
106
|
+
const emitRaw = vi.fn();
|
|
107
|
+
const client = {
|
|
108
|
+
opts: {
|
|
109
|
+
transport: { send: transportSend },
|
|
110
|
+
traceIdFactory: () => "trace-raw",
|
|
111
|
+
},
|
|
112
|
+
emitRaw,
|
|
113
|
+
} as unknown as ClawlingChatClient;
|
|
114
|
+
|
|
115
|
+
emitStreamCreated(client, {
|
|
116
|
+
messageId: "msg-1",
|
|
117
|
+
routing: { chatId: "chat-1", chatType: "group" },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(emitRaw).toHaveBeenCalledWith(
|
|
121
|
+
"message.created",
|
|
122
|
+
{ message_id: "msg-1" },
|
|
123
|
+
{ chat_id: "chat-1" },
|
|
124
|
+
);
|
|
125
|
+
expect(transportSend).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("falls back to SDK emitRaw with chat_id routing when raw transport is unavailable", () => {
|
|
129
|
+
const emitRaw = vi.fn();
|
|
130
|
+
const client = { emitRaw } as unknown as ClawlingChatClient;
|
|
131
|
+
|
|
132
|
+
emitStreamCreated(client, {
|
|
133
|
+
messageId: "msg-1",
|
|
134
|
+
routing: { chatId: "chat-1", chatType: "direct" },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(emitRaw).toHaveBeenCalledWith(
|
|
138
|
+
"message.created",
|
|
139
|
+
{ message_id: "msg-1" },
|
|
140
|
+
{ chat_id: "chat-1" },
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("requires raw transport for final stream replies that carry a client message_id", () => {
|
|
145
|
+
const emitRaw = vi.fn();
|
|
146
|
+
const client = { emitRaw } as unknown as ClawlingChatClient;
|
|
147
|
+
|
|
148
|
+
expect(() =>
|
|
149
|
+
emitFinalStreamReply(client, {
|
|
150
|
+
messageId: "agent-stream-1",
|
|
151
|
+
routing: { chatId: "chat-1", chatType: "direct" },
|
|
152
|
+
replyTo: {
|
|
153
|
+
msgId: "user-msg-1",
|
|
154
|
+
previewId: "user-1",
|
|
155
|
+
nickName: "User 1",
|
|
156
|
+
fragments: [{ kind: "text", text: "hello" }],
|
|
157
|
+
},
|
|
158
|
+
body: { fragments: [{ kind: "text", text: "reply" }] },
|
|
159
|
+
}),
|
|
160
|
+
).toThrow(/raw transport/);
|
|
161
|
+
expect(emitRaw).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
103
164
|
it("emitStreamAdd emits message.add with fragments: [{ text: full, delta: new }]", async () => {
|
|
104
165
|
const transport = new MockTransport();
|
|
105
166
|
const client = createOpenclawClawlingClient(baseAccount(), { transport });
|
package/src/client.ts
CHANGED
|
@@ -84,18 +84,26 @@ function emitEnvelope(
|
|
|
84
84
|
event: string,
|
|
85
85
|
payload: object,
|
|
86
86
|
routing: EnvelopeRouting,
|
|
87
|
+
options: { forceRawTransport?: boolean } = {},
|
|
87
88
|
): void {
|
|
88
89
|
const inner = client as unknown as {
|
|
89
90
|
opts?: {
|
|
90
91
|
transport: { send: (data: string) => void };
|
|
91
92
|
traceIdFactory: () => string;
|
|
92
93
|
};
|
|
93
|
-
emitRaw?: (event: string, payload: object, routing?: {
|
|
94
|
+
emitRaw?: (event: string, payload: object, routing?: { chat_id?: string; chat_type?: ChatType }) => void;
|
|
94
95
|
};
|
|
95
|
-
if (!inner.
|
|
96
|
-
inner.emitRaw
|
|
96
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
97
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
100
|
+
if (!inner.opts?.transport) {
|
|
101
|
+
if (!options.forceRawTransport && inner.emitRaw) {
|
|
102
|
+
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
|
|
106
|
+
}
|
|
99
107
|
const env = {
|
|
100
108
|
version: "2" as const,
|
|
101
109
|
event,
|
|
@@ -265,6 +273,7 @@ export function emitFinalStreamReply(
|
|
|
265
273
|
},
|
|
266
274
|
},
|
|
267
275
|
routing,
|
|
276
|
+
{ forceRawTransport: true },
|
|
268
277
|
);
|
|
269
278
|
}
|
|
270
279
|
|
package/src/manifest.test.ts
CHANGED
|
@@ -248,51 +248,41 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
248
248
|
expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it("documents
|
|
251
|
+
it("documents the numbered install restart step", () => {
|
|
252
252
|
const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
|
|
253
|
-
const restart = install.indexOf("## Restart
|
|
254
|
-
const activate = install.indexOf("## Activate");
|
|
253
|
+
const restart = install.indexOf("## 3. Restart");
|
|
254
|
+
const activate = install.indexOf("## 4. Activate");
|
|
255
255
|
const installSection = install.slice(restart, activate);
|
|
256
256
|
|
|
257
|
-
expect(installSection).toMatch(/perform a\s+real OpenClaw Gateway restart/i);
|
|
258
|
-
expect(installSection).toMatch(/Do not rely on hot reload or channel reload/i);
|
|
259
257
|
expect(installSection).toMatch(/openclaw gateway restart/);
|
|
260
|
-
expect(installSection).toMatch(/
|
|
261
|
-
expect(installSection).toMatch(/send a new message to continue/i);
|
|
258
|
+
expect(installSection).toMatch(/First restart completed/);
|
|
262
259
|
expect(installSection).not.toMatch(/kill -TERM 1/);
|
|
263
260
|
expect(installSection).not.toMatch(/docker restart <container>/);
|
|
264
|
-
expect(installSection).not.toMatch(/runtime imports the plugin/i);
|
|
265
|
-
expect(installSection).not.toMatch(/If the Gateway is already running/i);
|
|
266
261
|
});
|
|
267
262
|
|
|
268
|
-
it("documents activation
|
|
263
|
+
it("documents numbered CLI activation, second restart, and verification", () => {
|
|
269
264
|
const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
|
|
270
|
-
const activate = install.indexOf("## Activate");
|
|
271
|
-
const restartAgain = install.indexOf("## Restart Again");
|
|
272
|
-
const verify = install.indexOf("## Verify");
|
|
265
|
+
const activate = install.indexOf("## 4. Activate");
|
|
266
|
+
const restartAgain = install.indexOf("## 5. Restart Again");
|
|
267
|
+
const verify = install.indexOf("## 6. Verify");
|
|
273
268
|
const activateSection = install.slice(activate, restartAgain);
|
|
274
269
|
|
|
275
|
-
expect(activateSection).toMatch(/After the Gateway has restarted and is reachable/i);
|
|
276
|
-
expect(activateSection).toMatch(/clawchat_activate/);
|
|
277
|
-
expect(activateSection).toMatch(/\/clawchat-login XXXXXX/);
|
|
278
270
|
expect(activateSection).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/);
|
|
279
|
-
expect(activateSection).toMatch(/
|
|
280
|
-
expect(activateSection).not.toMatch(/OpenClaw 2026\.5\.5/i);
|
|
281
|
-
expect(activateSection).not.toMatch(/Unknown channel: openclaw-clawchat/i);
|
|
271
|
+
expect(activateSection).toMatch(/Activation completed/);
|
|
282
272
|
expect(activateSection).not.toMatch(/openclaw channels status --probe/i);
|
|
283
|
-
expect(activateSection).not.toMatch(/verify the channel/i);
|
|
284
273
|
|
|
285
274
|
const restartAgainSection = install.slice(
|
|
286
275
|
restartAgain,
|
|
287
|
-
|
|
276
|
+
verify,
|
|
288
277
|
);
|
|
289
|
-
expect(restartAgainSection).toMatch(/After activation succeeds/i);
|
|
290
|
-
expect(restartAgainSection).toMatch(/one more real Gateway restart/i);
|
|
291
278
|
expect(restartAgainSection).toMatch(/openclaw gateway restart/i);
|
|
279
|
+
expect(restartAgainSection).toMatch(/Second restart completed/);
|
|
292
280
|
|
|
293
281
|
const verifySection = install.slice(verify);
|
|
282
|
+
expect(verifySection).toMatch(/sleep 5/);
|
|
294
283
|
expect(verifySection).toMatch(/openclaw channels status --probe/i);
|
|
284
|
+
expect(verifySection).toMatch(/Verification completed/);
|
|
295
285
|
expect(verifySection).toMatch(/enabled, configured, running, and\s+connected/i);
|
|
296
|
-
expect(verifySection).toMatch(/
|
|
286
|
+
expect(verifySection).toMatch(/installation flow is complete/i);
|
|
297
287
|
});
|
|
298
288
|
});
|
|
@@ -130,6 +130,32 @@ describe("uploadOutboundMedia", () => {
|
|
|
130
130
|
]);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("passes host media access options to loadWebMedia", async () => {
|
|
134
|
+
const { runtime, loadWebMedia } = buildRuntime();
|
|
135
|
+
const apiClient = buildApiClient();
|
|
136
|
+
const readFile = vi.fn(async () => Buffer.from("host-read"));
|
|
137
|
+
|
|
138
|
+
await uploadOutboundMedia(["relative/image.png"], {
|
|
139
|
+
apiClient,
|
|
140
|
+
runtime,
|
|
141
|
+
mediaAccess: {
|
|
142
|
+
localRoots: ["/workspace"],
|
|
143
|
+
readFile,
|
|
144
|
+
workspaceDir: "/workspace",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(loadWebMedia).toHaveBeenCalledWith(
|
|
149
|
+
"relative/image.png",
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
localRoots: ["/workspace"],
|
|
152
|
+
readFile,
|
|
153
|
+
hostReadCapability: true,
|
|
154
|
+
workspaceDir: "/workspace",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
133
159
|
it("drops a single failed upload, returns the rest", async () => {
|
|
134
160
|
const { runtime } = buildRuntime();
|
|
135
161
|
const apiClient = buildApiClient();
|
package/src/media-runtime.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import {
|
|
3
|
+
buildOutboundMediaLoadOptions,
|
|
4
|
+
type OutboundMediaAccess,
|
|
5
|
+
type OutboundMediaReadFile,
|
|
6
|
+
} from "openclaw/plugin-sdk/media-runtime";
|
|
2
7
|
import type { OpenclawClawlingApiClient } from "./api-client.ts";
|
|
3
8
|
|
|
4
9
|
/**
|
|
@@ -42,8 +47,12 @@ export interface UploadOutboundCtx {
|
|
|
42
47
|
runtime: PluginRuntime;
|
|
43
48
|
log?: LogSink;
|
|
44
49
|
maxBytes?: number;
|
|
50
|
+
/** Host-authorized outbound media access from OpenClaw message delivery. */
|
|
51
|
+
mediaAccess?: OutboundMediaAccess;
|
|
45
52
|
/** Allowed local roots for path-based uploads. Empty/undefined = use loadWebMedia defaults. */
|
|
46
|
-
mediaLocalRoots?: readonly string[];
|
|
53
|
+
mediaLocalRoots?: readonly string[] | "any";
|
|
54
|
+
/** Host-provided read bridge for sandbox/allowed local media paths. */
|
|
55
|
+
mediaReadFile?: OutboundMediaReadFile;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export function inferMediaKindFromMime(mime: string | undefined): MediaItem["kind"] {
|
|
@@ -114,12 +123,15 @@ export async function uploadOutboundMedia(
|
|
|
114
123
|
const out: ClawlingMediaFragment[] = [];
|
|
115
124
|
for (const url of urls) {
|
|
116
125
|
try {
|
|
117
|
-
const loaded = await ctx.runtime.media.loadWebMedia(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
: {}),
|
|
122
|
-
|
|
126
|
+
const loaded = await ctx.runtime.media.loadWebMedia(
|
|
127
|
+
url,
|
|
128
|
+
buildOutboundMediaLoadOptions({
|
|
129
|
+
maxBytes,
|
|
130
|
+
...(ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {}),
|
|
131
|
+
...(ctx.mediaLocalRoots ? { mediaLocalRoots: ctx.mediaLocalRoots } : {}),
|
|
132
|
+
...(ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}),
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
123
135
|
const uploaded = await ctx.apiClient.uploadMedia({
|
|
124
136
|
buffer: loaded.buffer,
|
|
125
137
|
filename: loaded.fileName ?? "upload.bin",
|
package/src/outbound.test.ts
CHANGED
package/src/outbound.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface SendResult {
|
|
|
64
64
|
* - `clawchat:group:{chat_id}` → group
|
|
65
65
|
* - `openclaw-clawchat:direct:{chat_id}` → direct
|
|
66
66
|
* - `openclaw-clawchat:group:{chat_id}` → group
|
|
67
|
+
* - `direct:{chat_id}` → direct (host-normalized)
|
|
68
|
+
* - `group:{chat_id}` → group (host-normalized)
|
|
67
69
|
* - bare `{chat_id}` → direct (backward compat)
|
|
68
70
|
*/
|
|
69
71
|
export function parseOpenclawRecipient(to: string): { chatId: string; chatType: ChatType } {
|
|
@@ -75,6 +77,11 @@ export function parseOpenclawRecipient(to: string): { chatId: string; chatType:
|
|
|
75
77
|
|
|
76
78
|
const scheme = raw.slice(0, firstColon).toLowerCase();
|
|
77
79
|
const rest = raw.slice(firstColon + 1);
|
|
80
|
+
if (scheme === "direct" || scheme === "group") {
|
|
81
|
+
const chatId = rest.trim();
|
|
82
|
+
if (!chatId) throw new Error(`openclaw-clawchat: missing chat_id in "${to}"`);
|
|
83
|
+
return { chatId, chatType: scheme };
|
|
84
|
+
}
|
|
78
85
|
if (scheme !== "cc" && scheme !== "clawchat" && scheme !== CHANNEL_ID) {
|
|
79
86
|
return { chatId: raw, chatType: "direct" };
|
|
80
87
|
}
|
|
@@ -127,7 +134,7 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
127
134
|
mode: "normal",
|
|
128
135
|
replyTo: {
|
|
129
136
|
msgId: params.replyCtx.replyToMessageId,
|
|
130
|
-
senderId: params.replyCtx.
|
|
137
|
+
senderId: params.replyCtx.replyPreviewSenderId,
|
|
131
138
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
132
139
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
133
140
|
},
|
|
@@ -217,7 +224,7 @@ export const openclawClawlingOutbound: ChannelOutboundAdapter = {
|
|
|
217
224
|
messageId: result?.messageId ?? `${account.userId}-${Date.now()}`,
|
|
218
225
|
};
|
|
219
226
|
},
|
|
220
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots }) => {
|
|
227
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
221
228
|
const account = resolveOpenclawClawlingAccount(cfg);
|
|
222
229
|
const client =
|
|
223
230
|
getOpenclawClawlingClient(account.accountId) ??
|
|
@@ -234,7 +241,9 @@ export const openclawClawlingOutbound: ChannelOutboundAdapter = {
|
|
|
234
241
|
const mediaFragments = await uploadOutboundMedia([mediaUrl.trim()], {
|
|
235
242
|
apiClient,
|
|
236
243
|
runtime,
|
|
244
|
+
...(mediaAccess ? { mediaAccess } : {}),
|
|
237
245
|
...(mediaLocalRoots ? { mediaLocalRoots } : {}),
|
|
246
|
+
...(mediaReadFile ? { mediaReadFile } : {}),
|
|
238
247
|
});
|
|
239
248
|
if (mediaFragments.length === 0) {
|
|
240
249
|
throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);
|
|
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
|
|
4
4
|
|
|
5
5
|
describe("openclaw-clawchat reply-dispatcher", () => {
|
|
6
|
-
it("uses
|
|
6
|
+
it("uses the inbound sender id in consolidated streaming reply previews", async () => {
|
|
7
7
|
let hooks:
|
|
8
8
|
| {
|
|
9
9
|
deliver?: (payload: { text?: string }, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
@@ -70,7 +70,7 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
70
70
|
context: {
|
|
71
71
|
reply: {
|
|
72
72
|
reply_preview: {
|
|
73
|
-
id: "
|
|
73
|
+
id: "user-1",
|
|
74
74
|
nick_name: "User 1",
|
|
75
75
|
},
|
|
76
76
|
},
|
|
@@ -489,6 +489,104 @@ describe("openclaw-clawchat reply-dispatcher", () => {
|
|
|
489
489
|
);
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
+
it("prefers mediaUrls over legacy mediaUrl so one image is not sent twice", async () => {
|
|
493
|
+
let hooks:
|
|
494
|
+
| {
|
|
495
|
+
deliver?: (payload: {
|
|
496
|
+
text?: string;
|
|
497
|
+
mediaUrl?: string;
|
|
498
|
+
mediaUrls?: string[];
|
|
499
|
+
}, info?: { kind: "tool" | "block" | "final" }) => Promise<void>;
|
|
500
|
+
}
|
|
501
|
+
| undefined;
|
|
502
|
+
const loadWebMedia = vi.fn(async (url: string) => ({
|
|
503
|
+
buffer: Buffer.from(`bytes:${url}`),
|
|
504
|
+
contentType: "image/png",
|
|
505
|
+
fileName: "image.png",
|
|
506
|
+
}));
|
|
507
|
+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
508
|
+
new Response(
|
|
509
|
+
JSON.stringify({
|
|
510
|
+
code: 0,
|
|
511
|
+
msg: "ok",
|
|
512
|
+
data: { url: "https://cdn/uploaded.png", size: 12, mime: "image/png" },
|
|
513
|
+
}),
|
|
514
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
const client = {
|
|
518
|
+
sendMessage: vi.fn().mockResolvedValue({
|
|
519
|
+
payload: { message_id: "server-m1", accepted_at: 1234 },
|
|
520
|
+
trace_id: "trace-media",
|
|
521
|
+
}),
|
|
522
|
+
replyMessage: vi.fn(),
|
|
523
|
+
typing: vi.fn(),
|
|
524
|
+
} as unknown as ClawlingChatClient;
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
createOpenclawClawlingReplyDispatcher({
|
|
528
|
+
cfg: {} as never,
|
|
529
|
+
runtime: {
|
|
530
|
+
media: { loadWebMedia },
|
|
531
|
+
channel: {
|
|
532
|
+
reply: {
|
|
533
|
+
resolveHumanDelayConfig: vi.fn(() => ({ enabled: false })),
|
|
534
|
+
createReplyDispatcherWithTyping: vi.fn((options) => {
|
|
535
|
+
hooks = options;
|
|
536
|
+
return { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn() };
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
} as never,
|
|
541
|
+
account: {
|
|
542
|
+
accountId: "default",
|
|
543
|
+
baseUrl: "https://api.example.com",
|
|
544
|
+
token: "tk",
|
|
545
|
+
userId: "agent-1",
|
|
546
|
+
replyMode: "static",
|
|
547
|
+
forwardThinking: true,
|
|
548
|
+
forwardToolCalls: false,
|
|
549
|
+
stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
|
|
550
|
+
} as never,
|
|
551
|
+
client,
|
|
552
|
+
target: { chatId: "chat-1", chatType: "direct" },
|
|
553
|
+
log: { info: vi.fn(), error: vi.fn() },
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await hooks?.deliver?.(
|
|
557
|
+
{
|
|
558
|
+
text: "look",
|
|
559
|
+
mediaUrl: "https://cdn/legacy.png",
|
|
560
|
+
mediaUrls: ["https://cdn/image.png"],
|
|
561
|
+
},
|
|
562
|
+
{ kind: "final" },
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
expect(loadWebMedia).toHaveBeenCalledTimes(1);
|
|
566
|
+
expect(loadWebMedia).toHaveBeenCalledWith("https://cdn/image.png", expect.any(Object));
|
|
567
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
568
|
+
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
569
|
+
expect.objectContaining({
|
|
570
|
+
chat_id: "chat-1",
|
|
571
|
+
body: {
|
|
572
|
+
fragments: [
|
|
573
|
+
{ kind: "text", text: "look" },
|
|
574
|
+
{
|
|
575
|
+
kind: "image",
|
|
576
|
+
url: "https://cdn/uploaded.png",
|
|
577
|
+
mime: "image/png",
|
|
578
|
+
size: 12,
|
|
579
|
+
name: "image.png",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
585
|
+
} finally {
|
|
586
|
+
fetchMock.mockRestore();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
492
590
|
it("includes rich interaction fragments in the consolidated streaming final reply", async () => {
|
|
493
591
|
let hooks:
|
|
494
592
|
| {
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "openclaw/plugin-sdk/interactive-runtime";
|
|
9
9
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
10
10
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
11
12
|
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
|
12
13
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
13
14
|
import {
|
|
@@ -363,7 +364,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
363
364
|
routing,
|
|
364
365
|
replyTo: {
|
|
365
366
|
msgId: inboundMessageId ?? streamingMessageId,
|
|
366
|
-
previewId: inboundForFinalReply?.
|
|
367
|
+
previewId: inboundForFinalReply?.senderId ?? target.chatId,
|
|
367
368
|
nickName:
|
|
368
369
|
inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
|
|
369
370
|
fragments: inboundForFinalReply?.bodyText
|
|
@@ -379,10 +380,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
379
380
|
finalRichFragments.push(richFragment);
|
|
380
381
|
}
|
|
381
382
|
if (text) streamText = mergeStreamingText(streamText, text);
|
|
382
|
-
const urls =
|
|
383
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
384
|
-
...(payload.mediaUrls ?? []),
|
|
385
|
-
].filter((u): u is string => Boolean(u));
|
|
383
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
386
384
|
for (const url of urls) {
|
|
387
385
|
if (!accumulatedMediaUrls.includes(url)) accumulatedMediaUrls.push(url);
|
|
388
386
|
}
|
|
@@ -422,10 +420,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
422
420
|
deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
|
|
423
421
|
const richFragment = buildRichInteractionFragment(payload);
|
|
424
422
|
const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
|
|
425
|
-
const urls =
|
|
426
|
-
...(payload.mediaUrl ? [payload.mediaUrl] : []),
|
|
427
|
-
...(payload.mediaUrls ?? []),
|
|
428
|
-
].filter((u): u is string => Boolean(u));
|
|
423
|
+
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
429
424
|
log?.info?.(
|
|
430
425
|
`[${account.accountId}] openclaw-clawchat deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`,
|
|
431
426
|
);
|
package/src/runtime.test.ts
CHANGED
|
@@ -205,6 +205,7 @@ describe("openclaw-clawchat runtime media ingest", () => {
|
|
|
205
205
|
expect(capturedCtx?.MediaPath).toBe("/cache/1.png");
|
|
206
206
|
expect(capturedCtx?.MediaPaths).toEqual(["/cache/1.png"]);
|
|
207
207
|
expect(capturedCtx?.From).toBe("openclaw-clawchat:chat-1");
|
|
208
|
+
expect(capturedCtx?.OriginatingTo).toBe("openclaw-clawchat:chat-1");
|
|
208
209
|
expect(capturedCtx?.ConversationLabel).toBe("chat-1");
|
|
209
210
|
expect(capturedCtx?.SenderId).toBe("user-1");
|
|
210
211
|
expect(resolveAgentRoute).toHaveBeenCalledWith(
|
|
@@ -359,6 +360,7 @@ describe("openclaw-clawchat runtime media ingest", () => {
|
|
|
359
360
|
await startPromise;
|
|
360
361
|
|
|
361
362
|
expect(capturedCtx?.From).toBe("openclaw-clawchat:group:grp-1");
|
|
363
|
+
expect(capturedCtx?.OriginatingTo).toBe("openclaw-clawchat:group:grp-1");
|
|
362
364
|
expect(capturedCtx?.ConversationLabel).toBe("group:grp-1");
|
|
363
365
|
expect(capturedCtx?.SenderId).toBe("user-1");
|
|
364
366
|
expect(capturedCtx?.ChatType).toBe("group");
|
package/src/runtime.ts
CHANGED
|
@@ -200,16 +200,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
200
200
|
timestamp: turn.timestamp,
|
|
201
201
|
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
202
202
|
});
|
|
203
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
203
204
|
const ctxPayload = rt.reply.finalizeInboundContext({
|
|
204
205
|
Body: body,
|
|
205
206
|
BodyForAgent: turn.rawBody,
|
|
206
207
|
RawBody: turn.rawBody,
|
|
207
208
|
CommandBody: turn.rawBody,
|
|
208
|
-
// Clawling v2 routes by chat_id. `
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
|
|
209
|
+
// Clawling v2 routes by chat_id. `OriginatingTo` is what the
|
|
210
|
+
// message tool uses as the implicit current-chat target, so keep it
|
|
211
|
+
// on the conversation id rather than the agent account user id.
|
|
212
|
+
From: conversationTarget,
|
|
213
213
|
To: `${CHANNEL_ID}:${account.userId}`,
|
|
214
214
|
SessionKey: route.sessionKey,
|
|
215
215
|
AccountId: route.accountId ?? accountId,
|
|
@@ -222,7 +222,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
222
222
|
MessageSidFull: turn.messageId,
|
|
223
223
|
Timestamp: turn.timestamp,
|
|
224
224
|
OriginatingChannel: CHANNEL_ID,
|
|
225
|
-
OriginatingTo:
|
|
225
|
+
OriginatingTo: conversationTarget,
|
|
226
226
|
});
|
|
227
227
|
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
228
228
|
const inboundPaths =
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
const root = new URL("../", import.meta.url);
|
|
5
|
+
|
|
6
|
+
function readRootFile(name: string): string {
|
|
7
|
+
return fs.readFileSync(new URL(name, root), "utf8");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("OpenClaw packaging scripts", () => {
|
|
11
|
+
it("packages the npm tarball and uploads it to the R2 openclaw prefix", () => {
|
|
12
|
+
const script = readRootFile("package_openclaw_plugin.sh");
|
|
13
|
+
|
|
14
|
+
expect(script).toMatch(/npm pack --silent/);
|
|
15
|
+
expect(script).toContain('R2_ENV_FILE="${R2_ENV_FILE:-.env.r2}"');
|
|
16
|
+
expect(script).toContain('set -a; source "$R2_ENV_FILE"; set +a');
|
|
17
|
+
expect(script).toContain(': "${AWS_ACCESS_KEY_ID:?missing in $R2_ENV_FILE}"');
|
|
18
|
+
expect(script).toContain(': "${AWS_SECRET_ACCESS_KEY:?missing in $R2_ENV_FILE}"');
|
|
19
|
+
expect(script).toContain(': "${R2_ENDPOINT:?missing in $R2_ENV_FILE}"');
|
|
20
|
+
expect(script).toContain(': "${R2_BUCKET:?missing in $R2_ENV_FILE}"');
|
|
21
|
+
expect(script).not.toContain("f8eeaffb6f81ffd82b59c24d4ff797c9");
|
|
22
|
+
expect(script).not.toContain("587f8d4aa485a70f6e984a9efa7e609a86e4ef02d56a6682d9d7a9509913b8cb");
|
|
23
|
+
expect(script).toContain("OBJECT_KEY=\"openclaw/${TARGET_NAME}\"");
|
|
24
|
+
expect(script).toContain("https://dddddddddddddtest.clawling.chat/${OBJECT_KEY}");
|
|
25
|
+
expect(script).toMatch(/aws s3 cp[\s\S]*--content-type application\/gzip/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("keeps the local R2 env file out of git", () => {
|
|
29
|
+
const gitignore = readRootFile(".gitignore");
|
|
30
|
+
|
|
31
|
+
expect(gitignore).toMatch(/^\.env\.r2$/m);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("downloads the R2 tarball and installs it through openclaw", () => {
|
|
35
|
+
const script = readRootFile("install_openclaw.sh");
|
|
36
|
+
|
|
37
|
+
expect(script).toContain("PUBLIC_BASE_URL=\"https://dddddddddddddtest.clawling.chat\"");
|
|
38
|
+
expect(script).toContain("CLAWCHAT_PLUGIN_URL=\"${1:-${PUBLIC_BASE_URL}/openclaw/${DEFAULT_TGZ}}\"");
|
|
39
|
+
expect(script).toContain("curl -fL \"$CLAWCHAT_PLUGIN_URL\" -o \"$CLAWCHAT_PLUGIN_TGZ\"");
|
|
40
|
+
expect(script).toContain("openclaw plugins install \"$CLAWCHAT_PLUGIN_TGZ\" --force");
|
|
41
|
+
});
|
|
42
|
+
});
|