@clawling/clawchat-plugin-openclaw 2026.5.12-30 → 2026.5.12-32

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.
Files changed (62) hide show
  1. package/README.md +3 -48
  2. package/dist/src/api-client.js +37 -4
  3. package/dist/src/commands.js +4 -6
  4. package/dist/src/config.js +40 -0
  5. package/dist/src/login.runtime.js +1 -0
  6. package/dist/src/outbound.js +1 -0
  7. package/dist/src/reply-dispatcher.js +66 -14
  8. package/dist/src/runtime.js +1 -0
  9. package/dist/src/tools-schema.js +32 -0
  10. package/dist/src/tools.js +129 -14
  11. package/openclaw.plugin.json +9 -1
  12. package/package.json +7 -3
  13. package/prompts/default-group-bio.md +14 -14
  14. package/prompts/default-owner-behavior.md +22 -22
  15. package/skills/clawchat/SKILL.md +5 -1
  16. package/src/api-client.ts +61 -5
  17. package/src/api-types.ts +17 -0
  18. package/src/commands.ts +4 -6
  19. package/src/config.ts +51 -0
  20. package/src/login.runtime.ts +1 -0
  21. package/src/outbound.ts +1 -0
  22. package/src/reply-dispatcher.ts +65 -16
  23. package/src/runtime.ts +1 -0
  24. package/src/tools-schema.ts +47 -0
  25. package/src/tools.ts +169 -13
  26. package/INSTALL.md +0 -64
  27. package/dist/src/api-types.test-d.js +0 -10
  28. package/dist/src/protocol-types.typecheck.js +0 -1
  29. package/src/api-client.test.ts +0 -827
  30. package/src/channel.outbound.test.ts +0 -433
  31. package/src/channel.test.ts +0 -279
  32. package/src/clawchat-memory.test.ts +0 -482
  33. package/src/clawchat-metadata.test.ts +0 -477
  34. package/src/client.test.ts +0 -169
  35. package/src/commands.test.ts +0 -108
  36. package/src/config.test.ts +0 -352
  37. package/src/group-message-coalescer.test.ts +0 -227
  38. package/src/inbound.test.ts +0 -508
  39. package/src/llm-context-debug.test.ts +0 -55
  40. package/src/login.runtime.test.ts +0 -737
  41. package/src/manifest.test.ts +0 -360
  42. package/src/media-runtime.test.ts +0 -207
  43. package/src/message-mapper.test.ts +0 -201
  44. package/src/mock-transport.test.ts +0 -35
  45. package/src/outbound.test.ts +0 -1269
  46. package/src/plugin-entry.test.ts +0 -43
  47. package/src/plugin-prompts.test.ts +0 -94
  48. package/src/profile-prompt.test.ts +0 -290
  49. package/src/profile-sync.test.ts +0 -539
  50. package/src/prompt-injection.test.ts +0 -39
  51. package/src/protocol-types.test.ts +0 -69
  52. package/src/protocol-types.typecheck.ts +0 -89
  53. package/src/protocol.test.ts +0 -39
  54. package/src/reply-dispatcher.test.ts +0 -1352
  55. package/src/runtime.test.ts +0 -4920
  56. package/src/scripts.test.ts +0 -85
  57. package/src/storage.test.ts +0 -566
  58. package/src/terminal-send.test.ts +0 -81
  59. package/src/tools.test.ts +0 -933
  60. package/src/ws-alignment.test.ts +0 -103
  61. package/src/ws-client.test.ts +0 -1217
  62. package/src/ws-log.test.ts +0 -32
package/README.md CHANGED
@@ -20,13 +20,10 @@ npm i @clawling/clawchat-plugin-openclaw
20
20
 
21
21
  Requires `openclaw >= 2026.5.4` as a peer host.
22
22
 
23
- For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
24
- command documented in [`INSTALL.md`](./INSTALL.md).
25
-
26
23
  Example LLM prompt:
27
24
 
28
25
  ```text
29
- Use https://raw.githubusercontent.com/clawling/clawchat-plugin-openclaw/refs/heads/main/INSTALL.md to install and activate the ClawChat plugin. The invite code is XXXXXX.
26
+ Install and activate the ClawChat plugin from npm package @clawling/clawchat-plugin-openclaw. The invite code is XXXXXX.
30
27
  ```
31
28
 
32
29
  ## Current activation paths
@@ -165,7 +162,8 @@ npm run typecheck
165
162
 
166
163
  Tests live next to the source they cover (`*.test.ts`). The development entrypoint stays in TypeScript for the OpenClaw extension loader, while npm installs use the compiled runtime entrypoint generated by `npm run build` / `prepack` under `dist/`.
167
164
 
168
- Functional e2e test cases are documented in `.e2e/docs/install-clawchat-plugin-e2e.md`; keep that guide updated when adding or changing e2e flows.
165
+ Functional E2E test cases are documented in the root ClawChat workspace E2E
166
+ docs; keep that guide updated when adding or changing E2E flows.
169
167
 
170
168
  For OpenClaw host SDK/source lookup while developing this plugin, optionally
171
169
  clone OpenClaw into `tmp/openclaw`:
@@ -178,49 +176,6 @@ npm run dev:openclaw-source
178
176
  This checkout is local-only. It is ignored by git and is not required to run the
179
177
  plugin tests or publish the package.
180
178
 
181
- ## R2 package scripts
182
-
183
- Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
184
-
185
- ```bash
186
- ./scripts/package_openclaw_plugin.sh
187
- ```
188
-
189
- The script runs `npm pack`, removes `devDependencies` from the generated `.tgz`
190
- metadata so OpenClaw installs only runtime dependencies, uploads the `.tgz` to
191
- the configured R2 bucket, updates the `latest` R2 alias, uploads `INSTALL.md` as
192
- `openclaw/install.md`, and prints the public URLs. R2 credentials are read from
193
- `scripts/.env.r2`, which is ignored by git. Copy `scripts/.env.r2.example` to
194
- `scripts/.env.r2` and fill in the credentials. Use `--no-upload` to build the
195
- tarball without uploading it.
196
-
197
- ```bash
198
- AWS_ACCESS_KEY_ID=...
199
- AWS_SECRET_ACCESS_KEY=...
200
- AWS_DEFAULT_REGION=auto
201
- R2_ENDPOINT=https://...
202
- R2_BUCKET=...
203
- ```
204
-
205
- Install the R2-hosted latest tarball on a device or container with OpenClaw
206
- available:
207
-
208
- ```bash
209
- ./scripts/install_openclaw.sh
210
- ```
211
-
212
- To install a specific uploaded version, pass the version string:
213
-
214
- ```bash
215
- ./scripts/install_openclaw.sh 2026.5.16-1
216
- ```
217
-
218
- To install a specific uploaded tarball URL, pass its URL explicitly:
219
-
220
- ```bash
221
- ./scripts/install_openclaw.sh https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-2026.5.16-1.tgz
222
- ```
223
-
224
179
  ## License
225
180
 
226
181
  See the repository root.
@@ -5,9 +5,16 @@ export function createOpenclawClawlingApiClient(opts) {
5
5
  throw new ClawlingApiError("validation", `clawchat-plugin-openclaw baseUrl must start with http:// or https:// (got "${opts.baseUrl}")`);
6
6
  }
7
7
  const baseUrl = opts.baseUrl.replace(/\/+$/, "");
8
+ let mediaBaseUrl = baseUrl;
9
+ if (opts.mediaBaseUrl && opts.mediaBaseUrl.trim()) {
10
+ if (!/^https?:\/\//i.test(opts.mediaBaseUrl)) {
11
+ throw new ClawlingApiError("validation", `clawchat-plugin-openclaw mediaBaseUrl must start with http:// or https:// (got "${opts.mediaBaseUrl}")`);
12
+ }
13
+ mediaBaseUrl = opts.mediaBaseUrl.replace(/\/+$/, "");
14
+ }
8
15
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
9
- function url(path) {
10
- return `${baseUrl}${path}`;
16
+ function url(path, base = baseUrl) {
17
+ return `${base}${path}`;
11
18
  }
12
19
  function authHeaders(extra = {}) {
13
20
  // `X-Device-Id` is sent on every request so the server can correlate
@@ -75,7 +82,7 @@ export function createOpenclawClawlingApiClient(opts) {
75
82
  if (init?.body !== undefined) {
76
83
  requestInit.body = init.body;
77
84
  }
78
- res = await fetchImpl(url(path), requestInit);
85
+ res = await fetchImpl(url(path, init?.baseUrl || undefined), requestInit);
79
86
  }
80
87
  catch (err) {
81
88
  throw new ClawlingApiError("transport", `fetch failed: ${err instanceof Error ? err.message : String(err)}`, { path });
@@ -148,6 +155,32 @@ export function createOpenclawClawlingApiClient(opts) {
148
155
  async listFriends() {
149
156
  return await call("GET", "/v1/friendships");
150
157
  },
158
+ async sendFriendRequest(params) {
159
+ assertNonBlankId(params.userId, "sendFriendRequest: userId");
160
+ return await call("POST", "/v1/friendships", {
161
+ body: JSON.stringify({
162
+ user_id: params.userId,
163
+ ...(typeof params.greeting === "string" ? { greeting: params.greeting } : {}),
164
+ }),
165
+ headers: { "content-type": "application/json" },
166
+ });
167
+ },
168
+ async listFriendRequests(params) {
169
+ if (params.direction !== "incoming" && params.direction !== "outgoing") {
170
+ throw new ClawlingApiError("validation", "listFriendRequests: direction must be incoming or outgoing");
171
+ }
172
+ return await call("GET", `/v1/friendships/requests/${params.direction}`);
173
+ },
174
+ async acceptFriendRequest(requestId) {
175
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/accept`);
176
+ },
177
+ async rejectFriendRequest(requestId) {
178
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/reject`);
179
+ },
180
+ async removeFriend(friendUserId) {
181
+ assertNonBlankId(friendUserId, "removeFriend: friendUserId");
182
+ return await call("DELETE", `/v1/friendships/${encodeURIComponent(friendUserId)}`);
183
+ },
151
184
  async searchUsers(params) {
152
185
  const sp = new URLSearchParams();
153
186
  if (typeof params.q === "string")
@@ -245,7 +278,7 @@ export function createOpenclawClawlingApiClient(opts) {
245
278
  });
246
279
  const fd = new FormData();
247
280
  fd.set("file", file);
248
- const data = await call("POST", "/media/upload", { body: fd });
281
+ const data = await call("POST", "/media/upload", { body: fd, baseUrl: mediaBaseUrl });
249
282
  return parseUploadResult(data, "/media/upload");
250
283
  },
251
284
  async uploadAvatar(params) {
@@ -64,18 +64,16 @@ function persistOutputVisibility(draft, chatId, outputVisibility) {
64
64
  });
65
65
  }
66
66
  function formatOutputVisibilityResult(outputVisibility) {
67
- const runtimeStatus = outputVisibility === "full" ? "on" : "off";
68
67
  const detailLevel = {
69
- minimal: "quiet",
70
- normal: "normal",
71
- full: "verbose",
68
+ minimal: "final only",
69
+ normal: "final plus block media",
70
+ full: "final plus buffered reasoning, tool/progress, and block output",
72
71
  };
73
72
  return [
74
73
  "**ClawChat output updated**",
75
74
  "",
76
75
  `- visibility: \`${outputVisibility}\``,
77
- `- runtime status: \`${runtimeStatus}\``,
78
- `- detail level: \`${detailLevel[outputVisibility]}\``,
76
+ `- output: \`${detailLevel[outputVisibility]}\``,
79
77
  "",
80
78
  "Applies to new ClawChat messages.",
81
79
  ].join("\n");
@@ -7,6 +7,7 @@ export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID";
7
7
  export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
8
8
  export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
9
9
  export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
10
+ export const CLAWCHAT_MEDIA_BASE_URL_ENV = "CLAWCHAT_MEDIA_BASE_URL";
10
11
  /**
11
12
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
12
13
  * login` works out of the box without requiring a prior `openclaw channel
@@ -46,6 +47,7 @@ export const openclawClawlingConfigSchema = {
46
47
  enabled: { type: "boolean" },
47
48
  websocketUrl: { type: "string" },
48
49
  baseUrl: { type: "string" },
50
+ mediaBaseUrl: { type: "string" },
49
51
  token: { type: "string" },
50
52
  refreshToken: { type: "string" },
51
53
  agentId: { type: "string" },
@@ -189,6 +191,29 @@ function readGroupMode(value) {
189
191
  function readGroupCommandMode(value) {
190
192
  return value === "all" || value === "off" ? value : "owner";
191
193
  }
194
+ function readOutputVisibility(value) {
195
+ return value === "minimal" || value === "full" ? value : "normal";
196
+ }
197
+ function readOptionalOutputVisibility(value) {
198
+ return value === "minimal" || value === "normal" || value === "full" ? value : undefined;
199
+ }
200
+ function readChats(value) {
201
+ const rawChats = value && typeof value === "object" && !Array.isArray(value)
202
+ ? value
203
+ : {};
204
+ const chats = {};
205
+ for (const [chatId, rawChat] of Object.entries(rawChats)) {
206
+ if (!chatId)
207
+ continue;
208
+ const chat = rawChat && typeof rawChat === "object" && !Array.isArray(rawChat)
209
+ ? rawChat
210
+ : {};
211
+ const outputVisibility = readOptionalOutputVisibility(chat.outputVisibility);
212
+ if (outputVisibility)
213
+ chats[chatId] = { outputVisibility };
214
+ }
215
+ return chats;
216
+ }
192
217
  function readGroups(value) {
193
218
  const rawGroups = value && typeof value === "object" && !Array.isArray(value)
194
219
  ? value
@@ -200,9 +225,11 @@ function readGroups(value) {
200
225
  const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
201
226
  ? rawGroup
202
227
  : {};
228
+ const outputVisibility = readOptionalOutputVisibility(group.outputVisibility);
203
229
  groups[chatId] = {
204
230
  groupMode: readGroupMode(group.groupMode),
205
231
  groupCommandMode: readGroupCommandMode(group.groupCommandMode),
232
+ ...(outputVisibility ? { outputVisibility } : {}),
206
233
  };
207
234
  }
208
235
  return groups;
@@ -217,6 +244,13 @@ export function effectiveGroupCommandMode(account, chatId) {
217
244
  ?? account.groups["*"]?.groupCommandMode
218
245
  ?? account.groupCommandMode;
219
246
  }
247
+ export function effectiveOutputVisibility(account, chatId, chatType) {
248
+ return account.chats?.[chatId]?.outputVisibility
249
+ ?? (chatType === "group" ? account.groups?.[chatId]?.outputVisibility : undefined)
250
+ ?? (chatType === "group" ? account.groups?.["*"]?.outputVisibility : undefined)
251
+ ?? account.outputVisibility
252
+ ?? "normal";
253
+ }
220
254
  function readReconnect(raw) {
221
255
  const s = raw && typeof raw === "object" ? raw : {};
222
256
  return {
@@ -253,11 +287,14 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
253
287
  const baseUrl = readOptionalString(channel.baseUrl) ||
254
288
  readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
255
289
  DEFAULT_BASE_URL;
290
+ const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
256
291
  const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
257
292
  const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
258
293
  const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
259
294
  const ownerUserId = readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
260
295
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
296
+ const outputVisibility = readOutputVisibility(channel.outputVisibility);
297
+ const chats = readChats(channel.chats);
261
298
  const groupMode = readGroupMode(channel.groupMode);
262
299
  const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
263
300
  const groups = readGroups(channel.groups);
@@ -276,10 +313,13 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
276
313
  }),
277
314
  websocketUrl,
278
315
  baseUrl,
316
+ mediaBaseUrl,
279
317
  token,
280
318
  agentId,
281
319
  userId,
282
320
  ownerUserId,
321
+ outputVisibility,
322
+ chats,
283
323
  groupMode,
284
324
  groupCommandMode,
285
325
  groups,
@@ -131,6 +131,7 @@ export async function runOpenclawClawlingLogin(params) {
131
131
  }
132
132
  const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
133
133
  baseUrl: account.baseUrl,
134
+ mediaBaseUrl: account.mediaBaseUrl,
134
135
  // Pre-login we may not have a token yet. Send the current one (or empty)
135
136
  // — the server should accept an unauthenticated invite-code exchange.
136
137
  token: account.token || "",
@@ -581,6 +581,7 @@ export const openclawClawlingOutbound = {
581
581
  const runtime = getOpenclawClawlingRuntime();
582
582
  const apiClient = createOpenclawClawlingApiClient({
583
583
  baseUrl: account.baseUrl,
584
+ mediaBaseUrl: account.mediaBaseUrl,
584
585
  token: account.token,
585
586
  userId: account.userId,
586
587
  });
@@ -1,6 +1,7 @@
1
1
  import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
2
2
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
3
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
4
+ import { effectiveOutputVisibility, } from "./config.js";
4
5
  import { uploadOutboundMedia } from "./media-runtime.js";
5
6
  import { sendOpenclawClawlingText, } from "./outbound.js";
6
7
  import { isClawChatNoopResponseText } from "./profile-prompt.js";
@@ -106,15 +107,14 @@ function resolvePayloadText(payload) {
106
107
  /**
107
108
  * Reply dispatcher for clawchat-plugin-openclaw.
108
109
  *
109
- * The plugin intentionally forces complete-message delivery. It sets
110
- * `disableBlockStreaming: true` in reply options so OpenClaw does not split
111
- * deliver blocks for this channel. If the host still delivers non-final
112
- * blocks, the dispatcher buffers or ignores them and only emits materialized
113
- * `message.send` / `message.reply` frames for the final reply.
110
+ * ClawChat emits only materialized `message.send` / `message.reply` frames for
111
+ * the final reply. Non-final OpenClaw deliveries are ignored or buffered
112
+ * according to outputVisibility and are never sent as separate ClawChat frames.
114
113
  */
115
114
  export function createOpenclawClawlingReplyDispatcher(options) {
116
115
  const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
117
116
  const isGroupTarget = target.chatType === "group";
117
+ const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
118
118
  const ownerDirectTarget = () => {
119
119
  const ownerUserId = account.ownerUserId?.trim();
120
120
  return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
@@ -125,6 +125,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
125
125
  return null;
126
126
  return createOpenclawClawlingApiClient({
127
127
  baseUrl: account.baseUrl,
128
+ mediaBaseUrl: account.mediaBaseUrl,
128
129
  token: account.token,
129
130
  userId: account.userId,
130
131
  });
@@ -141,6 +142,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
141
142
  }
142
143
  // ----- Reply state ------------------------------------------------------
143
144
  let reasoningText = "";
145
+ let bufferedOutputText = "";
146
+ const bufferedOutputUrls = [];
144
147
  let runDone = false;
145
148
  let typingActive = false;
146
149
  let terminalReplySuppressed = false;
@@ -228,6 +231,37 @@ export function createOpenclawClawlingReplyDispatcher(options) {
228
231
  recordOutbound("thinking", messageId, thinkingText);
229
232
  reasoningText = "";
230
233
  };
234
+ const resetBufferedOutput = () => {
235
+ bufferedOutputText = "";
236
+ bufferedOutputUrls.length = 0;
237
+ };
238
+ const appendBufferedText = (value) => {
239
+ const trimmed = value.trim();
240
+ if (!trimmed)
241
+ return;
242
+ bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
243
+ };
244
+ const appendBufferedUrls = (urls) => {
245
+ for (const url of urls) {
246
+ if (url && !bufferedOutputUrls.includes(url))
247
+ bufferedOutputUrls.push(url);
248
+ }
249
+ };
250
+ const mergeFinalText = (text) => {
251
+ if (outputVisibility !== "full")
252
+ return text;
253
+ return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
254
+ };
255
+ const mergeFinalUrls = (urls) => {
256
+ if (outputVisibility === "minimal")
257
+ return urls;
258
+ const merged = bufferedOutputUrls.slice();
259
+ for (const url of urls) {
260
+ if (url && !merged.includes(url))
261
+ merged.push(url);
262
+ }
263
+ return merged;
264
+ };
231
265
  const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
232
266
  const emitTyping = (isTyping) => {
233
267
  if (!isTyping && !typingActive)
@@ -341,6 +375,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
341
375
  onReplyStart: async () => {
342
376
  emitTyping(true);
343
377
  reasoningText = "";
378
+ resetBufferedOutput();
344
379
  runDone = false;
345
380
  },
346
381
  deliver: async (payload, info) => {
@@ -361,13 +396,30 @@ export function createOpenclawClawlingReplyDispatcher(options) {
361
396
  return;
362
397
  }
363
398
  if (payload.isReasoning) {
364
- if (isGroupTarget || !account.forwardThinking)
399
+ if (isGroupTarget || outputVisibility !== "full")
365
400
  return;
366
- reasoningText = text;
401
+ appendBufferedText(text);
402
+ const trimmed = text.trim();
403
+ if (trimmed)
404
+ reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
405
+ return;
406
+ }
407
+ if (info?.kind === "tool") {
408
+ if (!isGroupTarget && outputVisibility === "full") {
409
+ appendBufferedText(text);
410
+ appendBufferedUrls(urls);
411
+ }
367
412
  return;
368
413
  }
369
- if (info?.kind === "tool")
414
+ if (info?.kind === "block") {
415
+ if (!isGroupTarget && outputVisibility === "normal")
416
+ appendBufferedUrls(urls);
417
+ if (!isGroupTarget && outputVisibility === "full") {
418
+ appendBufferedText(text);
419
+ appendBufferedUrls(urls);
420
+ }
370
421
  return;
422
+ }
371
423
  if (info?.kind === "final") {
372
424
  if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
373
425
  log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
@@ -390,15 +442,15 @@ export function createOpenclawClawlingReplyDispatcher(options) {
390
442
  });
391
443
  return;
392
444
  }
393
- const mediaFragments = await uploadMediaUrls(urls);
394
- const result = await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
445
+ const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
446
+ const finalUrls = mergeFinalUrls(urls);
447
+ const mediaFragments = await uploadMediaUrls(finalUrls);
448
+ const result = await sendStatic(finalText, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
395
449
  if (result?.messageId)
396
450
  recordThinkingIfLinked(result.messageId);
397
451
  return;
398
452
  }
399
- // kind === "block" or unknown: OpenClaw may still call this path while
400
- // the model is producing output. ClawChat gets only the final materialized
401
- // reply.
453
+ // Unknown delivery kind: keep ClawChat output tied to OpenClaw final.
402
454
  },
403
455
  onError: (error, info) => {
404
456
  const errorText = normalizeReplyErrorText(error);
@@ -423,7 +475,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
423
475
  replyOptions: {
424
476
  ...base.replyOptions,
425
477
  sourceReplyDeliveryMode: "automatic",
426
- disableBlockStreaming: true,
478
+ disableBlockStreaming: outputVisibility !== "full",
427
479
  },
428
480
  markDispatchIdle: base.markDispatchIdle,
429
481
  };
@@ -364,6 +364,7 @@ export async function startOpenclawClawlingGateway(params) {
364
364
  const getConversationApiClient = () => {
365
365
  conversationApiClient ??= createOpenclawClawlingApiClient({
366
366
  baseUrl: account.baseUrl,
367
+ mediaBaseUrl: account.mediaBaseUrl,
367
368
  token: account.token,
368
369
  userId: account.userId,
369
370
  });
@@ -76,6 +76,38 @@ export const ClawchatGetUserProfileSchema = Type.Object({
76
76
  }),
77
77
  });
78
78
  export const ClawchatListAccountFriendsSchema = Type.Object({});
79
+ export const ClawchatSendFriendRequestSchema = Type.Object({
80
+ userId: Type.String({
81
+ minLength: 1,
82
+ description: "Explicit target ClawChat user id to send a friend request to.",
83
+ }),
84
+ greeting: Type.Optional(Type.String({
85
+ description: "Optional greeting/message included with the friend request.",
86
+ })),
87
+ });
88
+ export const ClawchatListFriendRequestsSchema = Type.Object({
89
+ direction: Type.Optional(Type.Union([Type.Literal("incoming"), Type.Literal("outgoing")], {
90
+ description: "incoming lists requests sent to the connected account; outgoing lists requests sent by the connected account. Defaults to incoming.",
91
+ })),
92
+ });
93
+ export const ClawchatAcceptFriendRequestSchema = Type.Object({
94
+ requestId: Type.Integer({
95
+ minimum: 1,
96
+ description: "Concrete friend request id to accept.",
97
+ }),
98
+ });
99
+ export const ClawchatRejectFriendRequestSchema = Type.Object({
100
+ requestId: Type.Integer({
101
+ minimum: 1,
102
+ description: "Concrete friend request id to reject.",
103
+ }),
104
+ });
105
+ export const ClawchatRemoveFriendSchema = Type.Object({
106
+ friendUserId: Type.String({
107
+ minLength: 1,
108
+ description: "Explicit ClawChat user id of the accepted friend to remove.",
109
+ }),
110
+ });
79
111
  export const ClawchatSearchUsersSchema = Type.Object({
80
112
  q: Type.Optional(Type.String({
81
113
  description: "Search query for ClawChat username or nickname",