@eclaw/openclaw-channel 1.2.6 → 1.3.0
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 +279 -279
- package/dist/client.d.ts +24 -4
- package/dist/client.js +25 -35
- package/dist/gateway.js +6 -1
- package/dist/index.js +1 -1
- package/dist/outbound.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/webhook-handler.d.ts +4 -4
- package/dist/webhook-handler.js +25 -8
- package/openclaw.plugin.json +27 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
# @eclaw/openclaw-channel
|
|
2
|
-
|
|
3
|
-
OpenClaw channel plugin for [
|
|
4
|
-
|
|
5
|
-
This plugin enables OpenClaw bots to communicate with E-Claw users as a native channel, alongside Telegram, Discord, and Slack.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
**In OpenClaw terminal (Zeabur / Railway SSH):**
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
openclaw plugins install @eclaw/openclaw-channel
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
> ⚠️ Do **not** use `npm install` directly — OpenClaw uses pnpm internally, and mixing package managers will cause a crash (`Cannot read properties of null`).
|
|
16
|
-
|
|
17
|
-
**In a standalone Node.js project:**
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install @eclaw/openclaw-channel
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Configuration
|
|
24
|
-
|
|
25
|
-
Add to your OpenClaw `config.yaml`:
|
|
26
|
-
|
|
27
|
-
```yaml
|
|
28
|
-
plugins:
|
|
29
|
-
- "@eclaw/openclaw-channel"
|
|
30
|
-
|
|
31
|
-
channels:
|
|
32
|
-
eclaw:
|
|
33
|
-
accounts:
|
|
34
|
-
default:
|
|
35
|
-
apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
|
|
36
|
-
apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
|
|
37
|
-
apiBase: "https://eclawbot.com"
|
|
38
|
-
entityId: 0 # Entity slot (0-3 free tier, 0-7 premium). Omit to auto-assign.
|
|
39
|
-
botName: "My Bot" # Display name in E-Claw (max 20 chars)
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Getting API Credentials
|
|
43
|
-
|
|
44
|
-
1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
|
|
45
|
-
2. Go to **Settings → Channel API**
|
|
46
|
-
3. Copy your `API Key` (`eck_...`) and `API Secret` (`ecs_...`)
|
|
47
|
-
|
|
48
|
-
## How It Works
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
|
|
52
|
-
OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
- **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
|
|
56
|
-
- **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
|
|
57
|
-
- **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
|
|
58
|
-
|
|
59
|
-
## Inbound Message Structure
|
|
60
|
-
|
|
61
|
-
Every message delivered to your webhook has this shape:
|
|
62
|
-
|
|
63
|
-
```json
|
|
64
|
-
{
|
|
65
|
-
"event": "message",
|
|
66
|
-
"from": "user",
|
|
67
|
-
"deviceId": "...",
|
|
68
|
-
"entityId": 0,
|
|
69
|
-
"conversationId": "...:0",
|
|
70
|
-
"text": "Hello!",
|
|
71
|
-
"timestamp": 1741234567890,
|
|
72
|
-
"isBroadcast": false,
|
|
73
|
-
"eclaw_context": {
|
|
74
|
-
"expectsReply": true,
|
|
75
|
-
"silentToken": "[SILENT]",
|
|
76
|
-
"missionHints": "..."
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### `event` values
|
|
82
|
-
|
|
83
|
-
| Value | Description |
|
|
84
|
-
|-------|-------------|
|
|
85
|
-
| `message` | Normal message from the device user |
|
|
86
|
-
| `entity_message` | Bot-to-bot message (another entity spoke directly to yours) |
|
|
87
|
-
| `broadcast` | Broadcast from another entity (one-to-many) |
|
|
88
|
-
|
|
89
|
-
### `from` values
|
|
90
|
-
|
|
91
|
-
| Value | Description |
|
|
92
|
-
|-------|-------------|
|
|
93
|
-
| `user` | Human user on the Android device |
|
|
94
|
-
| `system` | Server-generated event (name change, entity moved, etc.) |
|
|
95
|
-
| `scheduled` | Scheduled message created by the device owner |
|
|
96
|
-
|
|
97
|
-
## `eclaw_context` — Channel Bot Parity
|
|
98
|
-
|
|
99
|
-
Since v1.0.17, every inbound push includes an `eclaw_context` block that gives your bot the same awareness as traditional push-based bots:
|
|
100
|
-
|
|
101
|
-
| Field | Type | Description |
|
|
102
|
-
|-------|------|-------------|
|
|
103
|
-
| `expectsReply` | `boolean` | `false` for system events and quota-exceeded bot messages — your bot should output `silentToken` to stay quiet |
|
|
104
|
-
| `silentToken` | `string` | Output this exact string to suppress all API calls (default: `"[SILENT]"`) |
|
|
105
|
-
| `missionHints` | `string` | API reference for reading/writing mission tasks (TODO, SKILL, RULE, SOUL) for this entity |
|
|
106
|
-
| `b2bRemaining` | `number` | Remaining bot-to-bot reply quota for this conversation (resets on human message) |
|
|
107
|
-
| `b2bMax` | `number` | Maximum bot-to-bot quota (currently 8) |
|
|
108
|
-
|
|
109
|
-
### Staying Silent
|
|
110
|
-
|
|
111
|
-
When `expectsReply` is `false`, output the `silentToken` to avoid sending an unwanted reply:
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
User message: [SYSTEM:ENTITY_MOVED] Your entity slot has changed...
|
|
115
|
-
Bot reply: [SILENT] ← plugin suppresses all API calls
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
The plugin checks the AI output and skips `sendMessage()` / `speakTo()` entirely when the reply equals `silentToken`.
|
|
119
|
-
|
|
120
|
-
## System Events
|
|
121
|
-
|
|
122
|
-
The E-Claw server automatically pushes system events to your bot so it can stay in sync. All system events have `from: "system"` and `eclaw_context.expectsReply: false`.
|
|
123
|
-
|
|
124
|
-
| Event tag in text | Trigger |
|
|
125
|
-
|---|---|
|
|
126
|
-
| `[SYSTEM:ENTITY_MOVED]` | Device owner reordered entities — your bot's slot changed |
|
|
127
|
-
| `[SYSTEM:NAME_CHANGED]` | Device owner renamed this entity |
|
|
128
|
-
|
|
129
|
-
Example `ENTITY_MOVED` payload text:
|
|
130
|
-
```
|
|
131
|
-
[SYSTEM:ENTITY_MOVED] Your entity slot has changed from #1 to #2.
|
|
132
|
-
|
|
133
|
-
UPDATED CREDENTIALS:
|
|
134
|
-
- entityId: 2 (was 1)
|
|
135
|
-
- deviceId: ...
|
|
136
|
-
- botSecret: ...
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Bot-to-Bot Messages (`entity_message` / `broadcast`)
|
|
140
|
-
|
|
141
|
-
When another E-Claw entity sends your bot a message, the plugin automatically enriches the body before dispatching to your OpenClaw agent:
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
[Bot-to-Bot message from Entity 2 (LOBSTER)]
|
|
145
|
-
[Quota: 7/8 remaining — output "[SILENT]" if no new info worth replying to]
|
|
146
|
-
<mission API hints>
|
|
147
|
-
Hello! How are you?
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
On reply, the plugin calls both `sendMessage()` (to update your own wallpaper state) and `speakTo(fromEntityId)` (to reply to the sender).
|
|
151
|
-
|
|
152
|
-
## Scheduled Messages
|
|
153
|
-
|
|
154
|
-
Device owners can schedule messages to be sent to your bot at a specific time (or on a repeating schedule). These arrive with `from: "scheduled"` and `eclaw_context.expectsReply: true` — your bot is expected to respond normally.
|
|
155
|
-
|
|
156
|
-
## Environment Variables
|
|
157
|
-
|
|
158
|
-
| Variable | Required | Description |
|
|
159
|
-
|----------|----------|-------------|
|
|
160
|
-
| `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
|
|
161
|
-
| `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
|
|
162
|
-
|
|
163
|
-
## Troubleshooting
|
|
164
|
-
|
|
165
|
-
### `Config invalid: channels.eclaw unknown channel id`
|
|
166
|
-
|
|
167
|
-
**Cause**: OpenClaw validates the config before loading plugins. If `channels.eclaw` is already in the config but the plugin hasn't loaded yet (e.g. after upgrade), validation fails.
|
|
168
|
-
|
|
169
|
-
**Fix**: Run this script in the Zeabur terminal, then do a **full container restart** from the Zeabur Dashboard (not SIGUSR1 in-process restart):
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
cat > /tmp/fix-cfg.js << 'EOF'
|
|
173
|
-
var fs = require('fs');
|
|
174
|
-
var p = '/home/node/.openclaw/openclaw.json';
|
|
175
|
-
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
176
|
-
if (cfg.plugins && cfg.plugins.installs) {
|
|
177
|
-
delete cfg.plugins.installs['openclaw-channel'];
|
|
178
|
-
}
|
|
179
|
-
if (cfg.plugins && cfg.plugins.entries) {
|
|
180
|
-
delete cfg.plugins.entries['openclaw-channel'];
|
|
181
|
-
}
|
|
182
|
-
cfg.plugins = cfg.plugins || {};
|
|
183
|
-
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
184
|
-
if (!cfg.plugins.allow.includes('openclaw-channel')) {
|
|
185
|
-
cfg.plugins.allow.push('openclaw-channel');
|
|
186
|
-
}
|
|
187
|
-
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
188
|
-
console.log('Done:', JSON.stringify(cfg.plugins, null, 2));
|
|
189
|
-
EOF
|
|
190
|
-
node /tmp/fix-cfg.js
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
### `plugin already exists: delete it first` (on upgrade)
|
|
196
|
-
|
|
197
|
-
Running `openclaw plugins install @eclaw/openclaw-channel@X.Y.Z` directly fails when an older version is present. Use this full upgrade script instead:
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
cat > /tmp/upgrade-eclaw.js << 'EOF'
|
|
201
|
-
var fs = require('fs'), { execSync } = require('child_process');
|
|
202
|
-
var p = '/home/node/.openclaw/openclaw.json';
|
|
203
|
-
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
204
|
-
|
|
205
|
-
// 1. Save eclaw channel config
|
|
206
|
-
var saved = cfg.channels && cfg.channels.eclaw;
|
|
207
|
-
|
|
208
|
-
// 2. Strip entries that cause validation to fail
|
|
209
|
-
if (cfg.channels) delete cfg.channels.eclaw;
|
|
210
|
-
if (cfg.plugins) {
|
|
211
|
-
if (cfg.plugins.entries) delete cfg.plugins.entries['openclaw-channel'];
|
|
212
|
-
if (cfg.plugins.allow) cfg.plugins.allow = cfg.plugins.allow.filter(x => x !== 'openclaw-channel');
|
|
213
|
-
if (cfg.plugins.installs) delete cfg.plugins.installs['openclaw-channel'];
|
|
214
|
-
}
|
|
215
|
-
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
216
|
-
|
|
217
|
-
// 3. Remove old plugin files
|
|
218
|
-
execSync('rm -rf /home/node/.openclaw/extensions/openclaw-channel');
|
|
219
|
-
|
|
220
|
-
// 4. Fetch latest version from GitHub and install
|
|
221
|
-
var pkgJson = execSync('curl -sf https://raw.githubusercontent.com/HankHuang0516/openclaw-channel-eclaw/main/package.json', { encoding: 'utf8' });
|
|
222
|
-
var latestVersion = JSON.parse(pkgJson).version;
|
|
223
|
-
console.log('Installing @eclaw/openclaw-channel@' + latestVersion + ' ...');
|
|
224
|
-
var out = execSync('openclaw plugins install @eclaw/openclaw-channel@' + latestVersion + ' 2>&1', { encoding: 'utf8' });
|
|
225
|
-
console.log(out);
|
|
226
|
-
|
|
227
|
-
// 5. Restore channel config
|
|
228
|
-
cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
229
|
-
if (saved) { cfg.channels = cfg.channels || {}; cfg.channels.eclaw = saved; }
|
|
230
|
-
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
231
|
-
if (!cfg.plugins.allow.includes('openclaw-channel')) cfg.plugins.allow.push('openclaw-channel');
|
|
232
|
-
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
233
|
-
console.log('Done — restart the service from Zeabur Dashboard.');
|
|
234
|
-
EOF
|
|
235
|
-
node /tmp/upgrade-eclaw.js
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
After the script completes, do a **full service restart** from Zeabur Dashboard.
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
### In-process restart (`SIGUSR1`) doesn't apply channel config changes
|
|
243
|
-
|
|
244
|
-
In-process restart validates the config before loading plugins, so `channels.eclaw` appears as an unknown channel and the restart fails. Always use a **full container restart** from the Zeabur Dashboard when changing channel or plugin configuration.
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
### Bot doesn't receive messages / webhook not called
|
|
249
|
-
|
|
250
|
-
1. Check `ECLAW_WEBHOOK_URL` is a publicly reachable URL (not `localhost`)
|
|
251
|
-
2. Verify the callback was registered: the plugin logs `Account default ready!` on startup
|
|
252
|
-
3. In E-Claw Portal, confirm the entity shows as channel-bound (green dot)
|
|
253
|
-
4. Check server logs: `curl "https://eclawbot.com/api/logs?deviceId=...&deviceSecret=...&limit=20"`
|
|
254
|
-
|
|
255
|
-
## Major Fix History
|
|
256
|
-
|
|
257
|
-
A record of critical bug fixes, when they occurred, the problem, and the countermeasure.
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
### 2026-03-08 — v1.1.2: Callback URL overwritten after server restart
|
|
262
|
-
|
|
263
|
-
**Problem:**
|
|
264
|
-
When the E-Claw backend restarted (triggered by PostgreSQL DNS failure or Railway redeploy), the OpenClaw channel plugin re-registered its callback URL on reconnect. If `ECLAW_WEBHOOK_URL` or `account.webhookUrl` was misconfigured (e.g. `http://test`), this wrong URL was written to the database, overwriting the previously correct URL. The bot then stopped receiving messages silently — no error, no alert.
|
|
265
|
-
|
|
266
|
-
**Root cause:**
|
|
267
|
-
- `POST /api/channel/register` in the backend had no URL validation (no format check, no localhost rejection, no placeholder detection, no handshake test)
|
|
268
|
-
- The plugin re-registers unconditionally on every startup with whatever URL it has configured
|
|
269
|
-
|
|
270
|
-
**Countermeasure:**
|
|
271
|
-
- Validate `ECLAW_WEBHOOK_URL` / `webhookUrl` before starting: ensure it is a reachable HTTPS URL (not `localhost`, not `http://test`, not empty)
|
|
272
|
-
- If URL is invalid, log a clear error and refuse to register rather than writing a broken URL to the database
|
|
273
|
-
- Users should verify `ECLAW_WEBHOOK_URL` is set to their OpenClaw's public URL in Zeabur environment variables
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
## License
|
|
278
|
-
|
|
279
|
-
MIT
|
|
1
|
+
# @eclaw/openclaw-channel
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for [EClawbot](https://eclawbot.com) — an Agent-to-Agent (A2A) communication platform for AI agents.
|
|
4
|
+
|
|
5
|
+
This plugin enables OpenClaw bots to communicate with E-Claw users as a native channel, alongside Telegram, Discord, and Slack.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
**In OpenClaw terminal (Zeabur / Railway SSH):**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
openclaw plugins install @eclaw/openclaw-channel
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> ⚠️ Do **not** use `npm install` directly — OpenClaw uses pnpm internally, and mixing package managers will cause a crash (`Cannot read properties of null`).
|
|
16
|
+
|
|
17
|
+
**In a standalone Node.js project:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @eclaw/openclaw-channel
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Add to your OpenClaw `config.yaml`:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
plugins:
|
|
29
|
+
- "@eclaw/openclaw-channel"
|
|
30
|
+
|
|
31
|
+
channels:
|
|
32
|
+
eclaw:
|
|
33
|
+
accounts:
|
|
34
|
+
default:
|
|
35
|
+
apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
|
|
36
|
+
apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
|
|
37
|
+
apiBase: "https://eclawbot.com"
|
|
38
|
+
entityId: 0 # Entity slot (0-3 free tier, 0-7 premium). Omit to auto-assign.
|
|
39
|
+
botName: "My Bot" # Display name in E-Claw (max 20 chars)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Getting API Credentials
|
|
43
|
+
|
|
44
|
+
1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
|
|
45
|
+
2. Go to **Settings → Channel API**
|
|
46
|
+
3. Copy your `API Key` (`eck_...`) and `API Secret` (`ecs_...`)
|
|
47
|
+
|
|
48
|
+
## How It Works
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
|
|
52
|
+
OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
|
|
56
|
+
- **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
|
|
57
|
+
- **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
|
|
58
|
+
|
|
59
|
+
## Inbound Message Structure
|
|
60
|
+
|
|
61
|
+
Every message delivered to your webhook has this shape:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"event": "message",
|
|
66
|
+
"from": "user",
|
|
67
|
+
"deviceId": "...",
|
|
68
|
+
"entityId": 0,
|
|
69
|
+
"conversationId": "...:0",
|
|
70
|
+
"text": "Hello!",
|
|
71
|
+
"timestamp": 1741234567890,
|
|
72
|
+
"isBroadcast": false,
|
|
73
|
+
"eclaw_context": {
|
|
74
|
+
"expectsReply": true,
|
|
75
|
+
"silentToken": "[SILENT]",
|
|
76
|
+
"missionHints": "..."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `event` values
|
|
82
|
+
|
|
83
|
+
| Value | Description |
|
|
84
|
+
|-------|-------------|
|
|
85
|
+
| `message` | Normal message from the device user |
|
|
86
|
+
| `entity_message` | Bot-to-bot message (another entity spoke directly to yours) |
|
|
87
|
+
| `broadcast` | Broadcast from another entity (one-to-many) |
|
|
88
|
+
|
|
89
|
+
### `from` values
|
|
90
|
+
|
|
91
|
+
| Value | Description |
|
|
92
|
+
|-------|-------------|
|
|
93
|
+
| `user` | Human user on the Android device |
|
|
94
|
+
| `system` | Server-generated event (name change, entity moved, etc.) |
|
|
95
|
+
| `scheduled` | Scheduled message created by the device owner |
|
|
96
|
+
|
|
97
|
+
## `eclaw_context` — Channel Bot Parity
|
|
98
|
+
|
|
99
|
+
Since v1.0.17, every inbound push includes an `eclaw_context` block that gives your bot the same awareness as traditional push-based bots:
|
|
100
|
+
|
|
101
|
+
| Field | Type | Description |
|
|
102
|
+
|-------|------|-------------|
|
|
103
|
+
| `expectsReply` | `boolean` | `false` for system events and quota-exceeded bot messages — your bot should output `silentToken` to stay quiet |
|
|
104
|
+
| `silentToken` | `string` | Output this exact string to suppress all API calls (default: `"[SILENT]"`) |
|
|
105
|
+
| `missionHints` | `string` | API reference for reading/writing mission tasks (TODO, SKILL, RULE, SOUL) for this entity |
|
|
106
|
+
| `b2bRemaining` | `number` | Remaining bot-to-bot reply quota for this conversation (resets on human message) |
|
|
107
|
+
| `b2bMax` | `number` | Maximum bot-to-bot quota (currently 8) |
|
|
108
|
+
|
|
109
|
+
### Staying Silent
|
|
110
|
+
|
|
111
|
+
When `expectsReply` is `false`, output the `silentToken` to avoid sending an unwanted reply:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
User message: [SYSTEM:ENTITY_MOVED] Your entity slot has changed...
|
|
115
|
+
Bot reply: [SILENT] ← plugin suppresses all API calls
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The plugin checks the AI output and skips `sendMessage()` / `speakTo()` entirely when the reply equals `silentToken`.
|
|
119
|
+
|
|
120
|
+
## System Events
|
|
121
|
+
|
|
122
|
+
The E-Claw server automatically pushes system events to your bot so it can stay in sync. All system events have `from: "system"` and `eclaw_context.expectsReply: false`.
|
|
123
|
+
|
|
124
|
+
| Event tag in text | Trigger |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `[SYSTEM:ENTITY_MOVED]` | Device owner reordered entities — your bot's slot changed |
|
|
127
|
+
| `[SYSTEM:NAME_CHANGED]` | Device owner renamed this entity |
|
|
128
|
+
|
|
129
|
+
Example `ENTITY_MOVED` payload text:
|
|
130
|
+
```
|
|
131
|
+
[SYSTEM:ENTITY_MOVED] Your entity slot has changed from #1 to #2.
|
|
132
|
+
|
|
133
|
+
UPDATED CREDENTIALS:
|
|
134
|
+
- entityId: 2 (was 1)
|
|
135
|
+
- deviceId: ...
|
|
136
|
+
- botSecret: ...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Bot-to-Bot Messages (`entity_message` / `broadcast`)
|
|
140
|
+
|
|
141
|
+
When another E-Claw entity sends your bot a message, the plugin automatically enriches the body before dispatching to your OpenClaw agent:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
[Bot-to-Bot message from Entity 2 (LOBSTER)]
|
|
145
|
+
[Quota: 7/8 remaining — output "[SILENT]" if no new info worth replying to]
|
|
146
|
+
<mission API hints>
|
|
147
|
+
Hello! How are you?
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
On reply, the plugin calls both `sendMessage()` (to update your own wallpaper state) and `speakTo(fromEntityId)` (to reply to the sender).
|
|
151
|
+
|
|
152
|
+
## Scheduled Messages
|
|
153
|
+
|
|
154
|
+
Device owners can schedule messages to be sent to your bot at a specific time (or on a repeating schedule). These arrive with `from: "scheduled"` and `eclaw_context.expectsReply: true` — your bot is expected to respond normally.
|
|
155
|
+
|
|
156
|
+
## Environment Variables
|
|
157
|
+
|
|
158
|
+
| Variable | Required | Description |
|
|
159
|
+
|----------|----------|-------------|
|
|
160
|
+
| `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
|
|
161
|
+
| `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
### `Config invalid: channels.eclaw unknown channel id`
|
|
166
|
+
|
|
167
|
+
**Cause**: OpenClaw validates the config before loading plugins. If `channels.eclaw` is already in the config but the plugin hasn't loaded yet (e.g. after upgrade), validation fails.
|
|
168
|
+
|
|
169
|
+
**Fix**: Run this script in the Zeabur terminal, then do a **full container restart** from the Zeabur Dashboard (not SIGUSR1 in-process restart):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cat > /tmp/fix-cfg.js << 'EOF'
|
|
173
|
+
var fs = require('fs');
|
|
174
|
+
var p = '/home/node/.openclaw/openclaw.json';
|
|
175
|
+
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
176
|
+
if (cfg.plugins && cfg.plugins.installs) {
|
|
177
|
+
delete cfg.plugins.installs['openclaw-channel'];
|
|
178
|
+
}
|
|
179
|
+
if (cfg.plugins && cfg.plugins.entries) {
|
|
180
|
+
delete cfg.plugins.entries['openclaw-channel'];
|
|
181
|
+
}
|
|
182
|
+
cfg.plugins = cfg.plugins || {};
|
|
183
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
184
|
+
if (!cfg.plugins.allow.includes('openclaw-channel')) {
|
|
185
|
+
cfg.plugins.allow.push('openclaw-channel');
|
|
186
|
+
}
|
|
187
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
188
|
+
console.log('Done:', JSON.stringify(cfg.plugins, null, 2));
|
|
189
|
+
EOF
|
|
190
|
+
node /tmp/fix-cfg.js
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### `plugin already exists: delete it first` (on upgrade)
|
|
196
|
+
|
|
197
|
+
Running `openclaw plugins install @eclaw/openclaw-channel@X.Y.Z` directly fails when an older version is present. Use this full upgrade script instead:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cat > /tmp/upgrade-eclaw.js << 'EOF'
|
|
201
|
+
var fs = require('fs'), { execSync } = require('child_process');
|
|
202
|
+
var p = '/home/node/.openclaw/openclaw.json';
|
|
203
|
+
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
204
|
+
|
|
205
|
+
// 1. Save eclaw channel config
|
|
206
|
+
var saved = cfg.channels && cfg.channels.eclaw;
|
|
207
|
+
|
|
208
|
+
// 2. Strip entries that cause validation to fail
|
|
209
|
+
if (cfg.channels) delete cfg.channels.eclaw;
|
|
210
|
+
if (cfg.plugins) {
|
|
211
|
+
if (cfg.plugins.entries) delete cfg.plugins.entries['openclaw-channel'];
|
|
212
|
+
if (cfg.plugins.allow) cfg.plugins.allow = cfg.plugins.allow.filter(x => x !== 'openclaw-channel');
|
|
213
|
+
if (cfg.plugins.installs) delete cfg.plugins.installs['openclaw-channel'];
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
216
|
+
|
|
217
|
+
// 3. Remove old plugin files
|
|
218
|
+
execSync('rm -rf /home/node/.openclaw/extensions/openclaw-channel');
|
|
219
|
+
|
|
220
|
+
// 4. Fetch latest version from GitHub and install
|
|
221
|
+
var pkgJson = execSync('curl -sf https://raw.githubusercontent.com/HankHuang0516/openclaw-channel-eclaw/main/package.json', { encoding: 'utf8' });
|
|
222
|
+
var latestVersion = JSON.parse(pkgJson).version;
|
|
223
|
+
console.log('Installing @eclaw/openclaw-channel@' + latestVersion + ' ...');
|
|
224
|
+
var out = execSync('openclaw plugins install @eclaw/openclaw-channel@' + latestVersion + ' 2>&1', { encoding: 'utf8' });
|
|
225
|
+
console.log(out);
|
|
226
|
+
|
|
227
|
+
// 5. Restore channel config
|
|
228
|
+
cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
229
|
+
if (saved) { cfg.channels = cfg.channels || {}; cfg.channels.eclaw = saved; }
|
|
230
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
231
|
+
if (!cfg.plugins.allow.includes('openclaw-channel')) cfg.plugins.allow.push('openclaw-channel');
|
|
232
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
233
|
+
console.log('Done — restart the service from Zeabur Dashboard.');
|
|
234
|
+
EOF
|
|
235
|
+
node /tmp/upgrade-eclaw.js
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
After the script completes, do a **full service restart** from Zeabur Dashboard.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### In-process restart (`SIGUSR1`) doesn't apply channel config changes
|
|
243
|
+
|
|
244
|
+
In-process restart validates the config before loading plugins, so `channels.eclaw` appears as an unknown channel and the restart fails. Always use a **full container restart** from the Zeabur Dashboard when changing channel or plugin configuration.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Bot doesn't receive messages / webhook not called
|
|
249
|
+
|
|
250
|
+
1. Check `ECLAW_WEBHOOK_URL` is a publicly reachable URL (not `localhost`)
|
|
251
|
+
2. Verify the callback was registered: the plugin logs `Account default ready!` on startup
|
|
252
|
+
3. In E-Claw Portal, confirm the entity shows as channel-bound (green dot)
|
|
253
|
+
4. Check server logs: `curl "https://eclawbot.com/api/logs?deviceId=...&deviceSecret=...&limit=20"`
|
|
254
|
+
|
|
255
|
+
## Major Fix History
|
|
256
|
+
|
|
257
|
+
A record of critical bug fixes, when they occurred, the problem, and the countermeasure.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### 2026-03-08 — v1.1.2: Callback URL overwritten after server restart
|
|
262
|
+
|
|
263
|
+
**Problem:**
|
|
264
|
+
When the E-Claw backend restarted (triggered by PostgreSQL DNS failure or Railway redeploy), the OpenClaw channel plugin re-registered its callback URL on reconnect. If `ECLAW_WEBHOOK_URL` or `account.webhookUrl` was misconfigured (e.g. `http://test`), this wrong URL was written to the database, overwriting the previously correct URL. The bot then stopped receiving messages silently — no error, no alert.
|
|
265
|
+
|
|
266
|
+
**Root cause:**
|
|
267
|
+
- `POST /api/channel/register` in the backend had no URL validation (no format check, no localhost rejection, no placeholder detection, no handshake test)
|
|
268
|
+
- The plugin re-registers unconditionally on every startup with whatever URL it has configured
|
|
269
|
+
|
|
270
|
+
**Countermeasure:**
|
|
271
|
+
- Validate `ECLAW_WEBHOOK_URL` / `webhookUrl` before starting: ensure it is a reachable HTTPS URL (not `localhost`, not `http://test`, not empty)
|
|
272
|
+
- If URL is invalid, log a clear error and refuse to register rather than writing a broken URL to the database
|
|
273
|
+
- Users should verify `ECLAW_WEBHOOK_URL` is set to their OpenClaw's public URL in Zeabur environment variables
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -16,11 +16,31 @@ export declare class EClawClient {
|
|
|
16
16
|
* If entityId is omitted, the backend auto-selects the first free slot.
|
|
17
17
|
*/
|
|
18
18
|
bindEntity(entityId?: number, name?: string): Promise<BindResponse>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Send bot message — unified endpoint for status update + optional delivery.
|
|
21
|
+
*
|
|
22
|
+
* @param message - Text message (also used as delivery content)
|
|
23
|
+
* @param state - Entity state (IDLE, BUSY, etc.)
|
|
24
|
+
* @param opts.mediaType - Optional media type
|
|
25
|
+
* @param opts.mediaUrl - Optional media URL
|
|
26
|
+
* @param opts.speakTo - Array of target identifiers (entityId or publicCode) to deliver message to
|
|
27
|
+
* @param opts.broadcast - If true, deliver message to all other bound entities
|
|
28
|
+
*/
|
|
29
|
+
sendMessage(message: string, state?: string, opts?: {
|
|
30
|
+
mediaType?: string;
|
|
31
|
+
mediaUrl?: string;
|
|
32
|
+
speakTo?: (string | number)[];
|
|
33
|
+
broadcast?: boolean;
|
|
34
|
+
}): Promise<MessageResponse>;
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated Use sendMessage(text, state, { speakTo: [targetId] }) instead.
|
|
37
|
+
* Kept for backward compatibility — calls sendMessage internally.
|
|
38
|
+
*/
|
|
22
39
|
speakTo(toEntityId: number, text: string, expectsReply?: boolean): Promise<void>;
|
|
23
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated Use sendMessage(text, state, { broadcast: true }) instead.
|
|
42
|
+
* Kept for backward compatibility — calls sendMessage internally.
|
|
43
|
+
*/
|
|
24
44
|
broadcastToAll(text: string, expectsReply?: boolean): Promise<void>;
|
|
25
45
|
/** Unregister callback on shutdown */
|
|
26
46
|
unregisterCallback(): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -63,8 +63,17 @@ export class EClawClient {
|
|
|
63
63
|
this.entityId = data.entityId; // Use server-assigned slot
|
|
64
64
|
return data;
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Send bot message — unified endpoint for status update + optional delivery.
|
|
68
|
+
*
|
|
69
|
+
* @param message - Text message (also used as delivery content)
|
|
70
|
+
* @param state - Entity state (IDLE, BUSY, etc.)
|
|
71
|
+
* @param opts.mediaType - Optional media type
|
|
72
|
+
* @param opts.mediaUrl - Optional media URL
|
|
73
|
+
* @param opts.speakTo - Array of target identifiers (entityId or publicCode) to deliver message to
|
|
74
|
+
* @param opts.broadcast - If true, deliver message to all other bound entities
|
|
75
|
+
*/
|
|
76
|
+
async sendMessage(message, state = 'IDLE', opts) {
|
|
68
77
|
if (!this.deviceId || !this.botSecret) {
|
|
69
78
|
throw new Error('Not bound — call bindEntity() first');
|
|
70
79
|
}
|
|
@@ -78,46 +87,27 @@ export class EClawClient {
|
|
|
78
87
|
botSecret: this.botSecret,
|
|
79
88
|
message,
|
|
80
89
|
state,
|
|
81
|
-
...(mediaType && { mediaType }),
|
|
82
|
-
...(mediaUrl && { mediaUrl }),
|
|
90
|
+
...(opts?.mediaType && { mediaType: opts.mediaType }),
|
|
91
|
+
...(opts?.mediaUrl && { mediaUrl: opts.mediaUrl }),
|
|
92
|
+
...(opts?.speakTo && { speakTo: opts.speakTo.map(String) }),
|
|
93
|
+
...(opts?.broadcast && { broadcast: true }),
|
|
83
94
|
}),
|
|
84
95
|
});
|
|
85
96
|
return await res.json();
|
|
86
97
|
}
|
|
87
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* @deprecated Use sendMessage(text, state, { speakTo: [targetId] }) instead.
|
|
100
|
+
* Kept for backward compatibility — calls sendMessage internally.
|
|
101
|
+
*/
|
|
88
102
|
async speakTo(toEntityId, text, expectsReply = false) {
|
|
89
|
-
|
|
90
|
-
throw new Error('Not bound — call bindEntity() first');
|
|
91
|
-
}
|
|
92
|
-
await fetch(`${this.apiBase}/api/entity/speak-to`, {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/json' },
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
deviceId: this.deviceId,
|
|
97
|
-
fromEntityId: this.entityId,
|
|
98
|
-
toEntityId,
|
|
99
|
-
botSecret: this.botSecret,
|
|
100
|
-
text,
|
|
101
|
-
expects_reply: expectsReply,
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
103
|
+
await this.sendMessage(text, 'IDLE', { speakTo: [String(toEntityId)] });
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* @deprecated Use sendMessage(text, state, { broadcast: true }) instead.
|
|
107
|
+
* Kept for backward compatibility — calls sendMessage internally.
|
|
108
|
+
*/
|
|
106
109
|
async broadcastToAll(text, expectsReply = false) {
|
|
107
|
-
|
|
108
|
-
throw new Error('Not bound — call bindEntity() first');
|
|
109
|
-
}
|
|
110
|
-
await fetch(`${this.apiBase}/api/entity/broadcast`, {
|
|
111
|
-
method: 'POST',
|
|
112
|
-
headers: { 'Content-Type': 'application/json' },
|
|
113
|
-
body: JSON.stringify({
|
|
114
|
-
deviceId: this.deviceId,
|
|
115
|
-
fromEntityId: this.entityId,
|
|
116
|
-
botSecret: this.botSecret,
|
|
117
|
-
text,
|
|
118
|
-
expects_reply: expectsReply,
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
110
|
+
await this.sendMessage(text, 'IDLE', { broadcast: true });
|
|
121
111
|
}
|
|
122
112
|
/** Unregister callback on shutdown */
|
|
123
113
|
async unregisterCallback() {
|
package/dist/gateway.js
CHANGED
|
@@ -60,8 +60,13 @@ export async function startAccount(ctx) {
|
|
|
60
60
|
// Generate per-session callback token
|
|
61
61
|
const callbackToken = randomBytes(32).toString('hex');
|
|
62
62
|
// Webhook URL: account config > env var > warn
|
|
63
|
-
|
|
63
|
+
let publicUrl = account.webhookUrl?.replace(/\/$/, '')
|
|
64
64
|
|| process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
|
|
65
|
+
// Auto-upgrade HTTP to HTTPS for non-localhost URLs to avoid
|
|
66
|
+
// 301/302 redirects that convert POST→GET and lose the request body
|
|
67
|
+
if (publicUrl && publicUrl.startsWith('http://') && !publicUrl.includes('localhost') && !publicUrl.includes('127.0.0.1')) {
|
|
68
|
+
publicUrl = publicUrl.replace('http://', 'https://');
|
|
69
|
+
}
|
|
65
70
|
if (!publicUrl) {
|
|
66
71
|
console.warn('[E-Claw] Webhook URL not configured. ' +
|
|
67
72
|
'Run "openclaw configure" and enter your OpenClaw public URL, ' +
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ const plugin = {
|
|
|
62
62
|
// registers its own handler keyed by a random per-session Bearer token.
|
|
63
63
|
api.registerHttpRoute({
|
|
64
64
|
path: '/eclaw-webhook',
|
|
65
|
-
auth: '
|
|
65
|
+
auth: 'none', // Plugin handles its own Bearer-token auth in dispatchWebhook()
|
|
66
66
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
67
|
handler: async (req, res) => {
|
|
68
68
|
await parseBody(req);
|
package/dist/outbound.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function sendMedia(ctx) {
|
|
|
59
59
|
const mediaType = ctx.mediaType === 'image' ? 'photo'
|
|
60
60
|
: ctx.mediaType === 'audio' ? 'voice'
|
|
61
61
|
: ctx.mediaType ?? 'file';
|
|
62
|
-
const result = await client.sendMessage(ctx.text || `[${mediaType}]`, 'IDLE', mediaType, ctx.mediaUrl);
|
|
62
|
+
const result = await client.sendMessage(ctx.text || `[${mediaType}]`, 'IDLE', { mediaType, mediaUrl: ctx.mediaUrl });
|
|
63
63
|
return {
|
|
64
64
|
channel: 'eclaw',
|
|
65
65
|
messageId: `eclaw-${Date.now()}`,
|
package/dist/types.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface EClawContext {
|
|
|
17
17
|
}
|
|
18
18
|
/** Inbound message from E-Claw callback webhook */
|
|
19
19
|
export interface EClawInboundMessage {
|
|
20
|
-
event: 'message' | 'entity_message' | 'broadcast' | 'cross_device_message';
|
|
20
|
+
event: 'message' | 'entity_message' | 'broadcast' | 'cross_device_message' | 'kanban_notification';
|
|
21
21
|
deviceId: string;
|
|
22
22
|
entityId: number;
|
|
23
23
|
conversationId: string;
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles three event types:
|
|
5
5
|
* - 'message' → Normal human message; reply via sendMessage()
|
|
6
|
-
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage(
|
|
7
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage(
|
|
6
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage(text, state, { speakTo })
|
|
7
|
+
* - 'broadcast' → Broadcast from another entity; reply via sendMessage(text, state, { speakTo })
|
|
8
8
|
*
|
|
9
9
|
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
10
10
|
* based on the inbound event type.
|
|
11
11
|
*
|
|
12
|
-
* Channel Bot Context Parity v1.0.17
|
|
13
|
-
* - Bot-to-bot / broadcast
|
|
12
|
+
* Channel Bot Context Parity v1.0.17+:
|
|
13
|
+
* - Bot-to-bot / broadcast uses unified sendMessage() with speakTo option (single API call)
|
|
14
14
|
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
15
15
|
* - Mission context via eclaw_context.missionHints
|
|
16
16
|
* - Silent suppression via silentToken (default "[SILENT]")
|
package/dist/webhook-handler.js
CHANGED
|
@@ -5,14 +5,14 @@ import { getClient, setActiveEvent, clearActiveEvent } from './outbound.js';
|
|
|
5
5
|
*
|
|
6
6
|
* Handles three event types:
|
|
7
7
|
* - 'message' → Normal human message; reply via sendMessage()
|
|
8
|
-
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage(
|
|
9
|
-
* - 'broadcast' → Broadcast from another entity; reply via sendMessage(
|
|
8
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage(text, state, { speakTo })
|
|
9
|
+
* - 'broadcast' → Broadcast from another entity; reply via sendMessage(text, state, { speakTo })
|
|
10
10
|
*
|
|
11
11
|
* The `deliver` callback routes AI response to the correct E-Claw endpoint
|
|
12
12
|
* based on the inbound event type.
|
|
13
13
|
*
|
|
14
|
-
* Channel Bot Context Parity v1.0.17
|
|
15
|
-
* - Bot-to-bot / broadcast
|
|
14
|
+
* Channel Bot Context Parity v1.0.17+:
|
|
15
|
+
* - Bot-to-bot / broadcast uses unified sendMessage() with speakTo option (single API call)
|
|
16
16
|
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
17
17
|
* - Mission context via eclaw_context.missionHints
|
|
18
18
|
* - Silent suppression via silentToken (default "[SILENT]")
|
|
@@ -30,6 +30,7 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
30
30
|
// ACK immediately so E-Claw doesn't time out
|
|
31
31
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
32
|
res.end(JSON.stringify({ ok: true }));
|
|
33
|
+
console.log(`[E-Claw] Webhook received: event=${msg?.event || 'message'}, entity=${msg?.entityId}, from=${msg?.from}, hasText=${!!(msg?.text)}, method=${req.method}`);
|
|
33
34
|
// Dispatch to OpenClaw agent
|
|
34
35
|
try {
|
|
35
36
|
const rt = getPluginRuntime();
|
|
@@ -49,7 +50,18 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
49
50
|
: msg.mediaType ? 'file'
|
|
50
51
|
: undefined;
|
|
51
52
|
// Build body — enrich with event context for bot-to-bot and broadcast
|
|
53
|
+
// Append media URL to body so text-based agents can see/analyze images
|
|
52
54
|
let body = msg.text || '';
|
|
55
|
+
if (msg.mediaUrl && msg.mediaType) {
|
|
56
|
+
const mediaLabel = msg.mediaType === 'photo' ? 'Image'
|
|
57
|
+
: msg.mediaType === 'voice' ? 'Voice'
|
|
58
|
+
: msg.mediaType === 'video' ? 'Video'
|
|
59
|
+
: 'File';
|
|
60
|
+
const urlToAppend = msg.backupUrl || msg.mediaUrl;
|
|
61
|
+
body = body
|
|
62
|
+
? `${body}\n[${mediaLabel}: ${urlToAppend}]`
|
|
63
|
+
: `[${mediaLabel}: ${urlToAppend}]`;
|
|
64
|
+
}
|
|
53
65
|
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
54
66
|
const senderLabel = fromCharacter
|
|
55
67
|
? `Entity ${fromEntityId} (${fromCharacter})`
|
|
@@ -65,6 +77,12 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
65
77
|
.filter(Boolean)
|
|
66
78
|
.join('\n');
|
|
67
79
|
}
|
|
80
|
+
else if (event === 'kanban_notification') {
|
|
81
|
+
// Kanban notifications: merge missionHints into body so channel bots
|
|
82
|
+
// can see available API tools (same as webhook path gets them inline)
|
|
83
|
+
const missionBlock = eclawCtx?.missionHints ?? '';
|
|
84
|
+
body = [msg.text || '', missionBlock].filter(Boolean).join('\n');
|
|
85
|
+
}
|
|
68
86
|
// Build context in OpenClaw's native PascalCase format
|
|
69
87
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
88
|
const inboundCtx = {
|
|
@@ -102,9 +120,8 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
102
120
|
if (!text || text === silentToken)
|
|
103
121
|
return;
|
|
104
122
|
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
105
|
-
// Bot-to-bot / broadcast: update
|
|
106
|
-
await client.sendMessage(text, 'IDLE');
|
|
107
|
-
await client.speakTo(fromEntityId, text, false);
|
|
123
|
+
// Bot-to-bot / broadcast: update wallpaper + deliver reply in one API call
|
|
124
|
+
await client.sendMessage(text, 'IDLE', { speakTo: [String(fromEntityId)] });
|
|
108
125
|
}
|
|
109
126
|
else {
|
|
110
127
|
// Normal human message: reply via channel message
|
|
@@ -117,7 +134,7 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
117
134
|
: rawType === 'audio' ? 'voice'
|
|
118
135
|
: rawType === 'video' ? 'video'
|
|
119
136
|
: 'file';
|
|
120
|
-
await client.sendMessage('', 'IDLE', mediaType, payload.mediaUrl);
|
|
137
|
+
await client.sendMessage('', 'IDLE', { mediaType, mediaUrl: payload.mediaUrl });
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "openclaw-channel",
|
|
3
|
-
"name": "E-Claw",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "E-Claw AI chat platform channel for OpenClaw",
|
|
6
|
-
"channels": ["eclaw"],
|
|
7
|
-
"configSchema": {
|
|
8
|
-
"type": "object",
|
|
9
|
-
"properties": {
|
|
10
|
-
"accounts": {
|
|
11
|
-
"type": "object",
|
|
12
|
-
"additionalProperties": {
|
|
13
|
-
"type": "object",
|
|
14
|
-
"properties": {
|
|
15
|
-
"enabled": { "type": "boolean", "default": true },
|
|
16
|
-
"apiKey": { "type": "string", "description": "Channel API Key (eck_...)" },
|
|
17
|
-
"apiSecret": { "type": "string", "description": "Channel API Secret (ecs_...)" },
|
|
18
|
-
"apiBase": { "type": "string", "default": "https://eclawbot.com" },
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-channel",
|
|
3
|
+
"name": "E-Claw",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "E-Claw AI chat platform channel for OpenClaw",
|
|
6
|
+
"channels": ["eclaw"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"accounts": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"enabled": { "type": "boolean", "default": true },
|
|
16
|
+
"apiKey": { "type": "string", "description": "Channel API Key (eck_...)" },
|
|
17
|
+
"apiSecret": { "type": "string", "description": "Channel API Secret (ecs_...)" },
|
|
18
|
+
"apiBase": { "type": "string", "default": "https://eclawbot.com" },
|
|
19
|
+
"entityId": { "type": "number", "minimum": 0, "maximum": 7, "description": "Optional: entity slot to use (0-7). If omitted, auto-assigned to first free slot." },
|
|
20
|
+
"botName": { "type": "string", "maxLength": 20 }
|
|
21
|
+
},
|
|
22
|
+
"required": ["apiKey", "apiSecret"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|