Skip to content

Commit 0d8d759

Browse files
authored
fix(slack): retain partial stream participation
1 parent a3155ca commit 0d8d759

17 files changed

Lines changed: 387 additions & 11 deletions

File tree

.github/labeler.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@
171171
- any-glob-to-any-file:
172172
- "extensions/zalo/**"
173173
- "docs/channels/zalo.md"
174+
"channel: zaloclawbot":
175+
- changed-files:
176+
- any-glob-to-any-file:
177+
- "docs/channels/zaloclawbot.md"
174178
"channel: zalouser":
175179
- changed-files:
176180
- any-glob-to-any-file:

docs/.i18n/glossary.zh-CN.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,5 +1194,9 @@
11941194
{
11951195
"source": "cohere",
11961196
"target": "cohere"
1197+
},
1198+
{
1199+
"source": "Zalo ClawBot",
1200+
"target": "Zalo ClawBot"
11971201
}
11981202
]

docs/channels/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Text is supported everywhere; media and reactions vary by channel.
5252
- [WhatsApp](/channels/whatsapp) - Most popular; uses Baileys and requires QR pairing.
5353
- [Yuanbao](/channels/yuanbao) - Tencent Yuanbao bot (external plugin).
5454
- [Zalo](/channels/zalo) - Zalo Bot API; Vietnam's popular messenger (bundled plugin).
55+
- [Zalo ClawBot](/channels/zaloclawbot) - Personal Zalo assistant via QR login; owner-bound (external plugin).
5556
- [Zalo Personal](/channels/zalouser) - Zalo personal account via QR login (bundled plugin).
5657

5758
## Notes

docs/channels/zaloclawbot.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
summary: "Zalo ClawBot channel setup through the external openclaw-zaloclawbot plugin"
3+
read_when:
4+
- You want a personal Zalo assistant bot with QR-code login
5+
- You are installing or troubleshooting the openclaw-zaloclawbot channel plugin
6+
title: "Zalo ClawBot"
7+
---
8+
9+
OpenClaw connects to Zalo ClawBot through the catalog-listed external
10+
`@zalo-platforms/openclaw-zaloclawbot` plugin. Login uses a Zalo Mini App QR
11+
code.
12+
13+
## Compatibility
14+
15+
| Plugin Version | OpenClaw Version | npm dist-tag | Status |
16+
| -------------- | ---------------- | ------------ | ------------- |
17+
| 0.1.x | >=2026.4.10 | `latest` | Active / Beta |
18+
19+
## Prerequisites
20+
21+
- Node.js **>= 22**
22+
- [OpenClaw](https://docs.openclaw.ai/install) must be installed (`openclaw` CLI available).
23+
- A Zalo account on a mobile device to scan the login QR code.
24+
25+
## Install with onboard (recommended)
26+
27+
Run the OpenClaw onboarding wizard and pick **Zalo ClawBot** from the channel menu:
28+
29+
```bash
30+
openclaw onboard
31+
```
32+
33+
The wizard installs the plugin from the official catalog (integrity-verified), renders the login QR right in the terminal, and finishes the channel once you scan it with the Zalo app. No extra commands are needed.
34+
35+
## Manual Installation
36+
37+
To add the channel to an already-onboarded gateway, follow these steps:
38+
39+
### 1. Install the plugin
40+
41+
```bash
42+
openclaw plugins install "@zalo-platforms/[email protected]"
43+
```
44+
45+
Use the exact pinned version shown above (it matches the official catalog entry), so OpenClaw verifies the package against the catalog integrity hash during install.
46+
47+
### 2. Enable the plugin in config
48+
49+
```bash
50+
openclaw config set plugins.entries.openclaw-zaloclawbot.enabled true
51+
```
52+
53+
### 3. Generate QR code and log in
54+
55+
```bash
56+
openclaw channels login --channel openclaw-zaloclawbot
57+
```
58+
59+
Scan the terminal-rendered QR code using the Zalo mobile app, accept the Terms of Use inside the Zalo Mini App, and authorize the session.
60+
61+
### 4. Restart the gateway
62+
63+
```bash
64+
openclaw gateway restart
65+
```
66+
67+
---
68+
69+
## How It Works
70+
71+
Unlike the standard developer Zalo channel which requires you to register your own Zalo Official Account (OA) and paste static developer credentials, Zalo ClawBot operates as an **owner-bound personal assistant** using a shared, official infrastructure:
72+
73+
1. **Secure Onboarding:** The QR code resolves to a secure Zalo Mini App that binds a newly-provisioned, private bot under a shared official OA directly to your Zalo User ID.
74+
2. **Owner-Bound Privacy:** By design, the bot is restricted to communicating _only_ with its owner. Messages from other users are dropped at the platform level, making the connection private and secure.
75+
3. **Official API path:** The plugin uses Zalo Bot Platform APIs instead of
76+
browser or web-session automation.
77+
78+
## Under the Hood
79+
80+
The Zalo ClawBot plugin communicates with Zalo APIs via a persistent long-polling message loop. To maintain a clean and lightweight runtime:
81+
82+
- Long-poll connections utilize the `getUpdates` endpoint.
83+
- Webhooks are disabled by default for local desktop/terminal gateway runs.
84+
- Messages are processed client-side and mapped directly to your local agent runtime.
85+
86+
The external plugin manages bot credentials under the OpenClaw state directory.
87+
Treat that directory as sensitive and include it in the same access-control and
88+
backup policy as the rest of your OpenClaw state.
89+
90+
---
91+
92+
## Troubleshooting
93+
94+
- **QR Login Timeout:** The login token (`zbsk`) expires after 5 minutes for security reasons. If the QR code expires before you scan it, simply rerun the login command to generate a new one.
95+
- **Gateway Fails to Load:** Ensure your OpenClaw host version is `2026.4.10` or higher. Older versions do not support the external npm-plugin installation ledger.

docs/docs.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@
316316
"source": "/providers/zalo",
317317
"destination": "/channels/zalo"
318318
},
319+
{
320+
"source": "/channels/openclaw-zaloclawbot",
321+
"destination": "/channels/zaloclawbot"
322+
},
319323
{
320324
"source": "/providers/whatsapp",
321325
"destination": "/channels/whatsapp"
@@ -1132,6 +1136,7 @@
11321136
"channels/feishu",
11331137
"channels/yuanbao",
11341138
"channels/zalo",
1139+
"channels/zaloclawbot",
11351140
"channels/zalouser"
11361141
]
11371142
},

docs/providers/xai.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,10 @@ Legacy aliases still normalize to the canonical bundled ids:
504504
sign-in URL. xAI decides which accounts can receive OAuth API tokens, and
505505
the consent page may show Grok Build even though OpenClaw does not require
506506
the Grok Build app.
507-
- `grok-4.20-multi-agent-experimental-beta-0304` is not supported on the
508-
normal xAI provider path because it requires a different upstream API
509-
surface than the standard OpenClaw xAI transport.
507+
- OpenClaw does not currently expose the xAI multi-agent model family. xAI
508+
serves these models through the Responses API, but they do not accept the
509+
client-side or custom tools used by OpenClaw's shared agent loop. See the
510+
[xAI multi-agent limitations](https://docs.x.ai/developers/model-capabilities/text/multi-agent#limitations).
510511
- xAI Realtime voice is not registered as an OpenClaw provider yet. It
511512
needs a different bidirectional voice session contract than batch STT or
512513
streaming transcription.

extensions/discord/src/send.webhook.proxy.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ vi.mock("openclaw/plugin-sdk/fetch-runtime", async () => {
1616
};
1717
});
1818

19+
function cancelTrackedResponse(
20+
text: string,
21+
init: ResponseInit,
22+
): {
23+
response: Response;
24+
wasCanceled: () => boolean;
25+
} {
26+
let canceled = false;
27+
const stream = new ReadableStream<Uint8Array>({
28+
start(controller) {
29+
controller.enqueue(new TextEncoder().encode(text));
30+
},
31+
cancel() {
32+
canceled = true;
33+
},
34+
});
35+
return {
36+
response: new Response(stream, init),
37+
wasCanceled: () => canceled,
38+
};
39+
}
40+
1941
describe("sendWebhookMessageDiscord proxy support", () => {
2042
beforeEach(() => {
2143
makeProxyFetchMock.mockReset();
@@ -208,4 +230,39 @@ describe("sendWebhookMessageDiscord proxy support", () => {
208230
expect(error.rawBody).toEqual({ message: "upstream unavailable" });
209231
globalFetchMock.mockRestore();
210232
});
233+
234+
it("bounds webhook error bodies without using response.text()", async () => {
235+
const tracked = cancelTrackedResponse(`${"upstream unavailable ".repeat(1024)}tail`, {
236+
status: 503,
237+
headers: { "content-type": "text/plain" },
238+
});
239+
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
240+
const globalFetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(tracked.response);
241+
242+
const cfg = {
243+
channels: {
244+
discord: {
245+
token: "Bot test-token",
246+
},
247+
},
248+
} as OpenClawConfig;
249+
250+
const thrown = await sendWebhookMessageDiscord("hello", {
251+
cfg,
252+
accountId: "default",
253+
webhookId: "123",
254+
webhookToken: "abc",
255+
wait: true,
256+
}).then(
257+
() => undefined,
258+
(error: unknown) => error,
259+
);
260+
expect(thrown).toBeInstanceOf(DiscordError);
261+
const error = thrown as DiscordError;
262+
expect(error.message).toContain("upstream unavailable");
263+
expect(JSON.stringify(error.rawBody)).not.toContain("tail");
264+
expect(tracked.wasCanceled()).toBe(true);
265+
expect(textSpy).not.toHaveBeenCalled();
266+
globalFetchMock.mockRestore();
267+
});
211268
});

extensions/discord/src/send.webhook.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Discord plugin module implements send.webhook behavior.
22
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
33
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
4+
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
45
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
56
import { resolveDiscordClientAccountContext } from "./client.js";
67
import {
@@ -14,6 +15,8 @@ import { rewriteDiscordKnownMentions } from "./mentions.js";
1415
import { createDiscordSendResult } from "./send.receipt.js";
1516
import type { DiscordSendResult } from "./send.types.js";
1617

18+
const DISCORD_WEBHOOK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
19+
1720
type DiscordWebhookSendOpts = {
1821
cfg: OpenClawConfig;
1922
webhookId: string;
@@ -54,7 +57,9 @@ function coerceWebhookErrorBody(raw: string): unknown {
5457
}
5558

5659
async function throwWebhookResponseError(response: Response): Promise<never> {
57-
const raw = await response.text().catch(() => "");
60+
const raw = await readResponseTextLimited(response, DISCORD_WEBHOOK_ERROR_BODY_LIMIT_BYTES).catch(
61+
() => "",
62+
);
5863
const parsed = coerceWebhookErrorBody(raw);
5964
if (response.status === 429) {
6065
throw new RateLimitError(response, {

extensions/whatsapp/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("whatsapp bundled entries", () => {
1515

1616
it("declares account config as channel-restart reload metadata", () => {
1717
expect(whatsappPlugin.reload).toEqual({
18-
configPrefixes: ["web", "channels.whatsapp.accounts"],
18+
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
1919
noopPrefixes: ["channels.whatsapp"],
2020
});
2121
});

extensions/whatsapp/src/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function createWhatsAppPluginBase(params: {
181181
// the broad `channels.whatsapp` noop prefix below otherwise swallows it as a
182182
// hot no-op and leaves the account connected until a full restart.
183183
reload: {
184-
configPrefixes: ["web", "channels.whatsapp.accounts"],
184+
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
185185
noopPrefixes: ["channels.whatsapp"],
186186
},
187187
gatewayMethodDescriptors: [{ name: "web.login.start" }, { name: "web.login.wait" }],

0 commit comments

Comments
 (0)