@eclaw/openclaw-channel 1.1.7 → 1.2.1

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 CHANGED
@@ -1,279 +1,279 @@
1
- # @eclaw/openclaw-channel
2
-
3
- OpenClaw channel plugin for [E-Claw](https://eclawbot.com) — an AI chat platform for live wallpaper entities on Android.
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 [E-Claw](https://eclawbot.com) — an AI chat platform for live wallpaper entities on Android.
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
@@ -11,7 +11,7 @@ export declare class EClawClient {
11
11
  private entityId;
12
12
  constructor(config: EClawAccountConfig);
13
13
  /** Register callback URL with E-Claw backend */
14
- registerCallback(callbackUrl: string, callbackToken: string, callbackUsername?: string, callbackPassword?: string): Promise<RegisterResponse>;
14
+ registerCallback(callbackUrl: string, callbackToken: string): Promise<RegisterResponse>;
15
15
  /** Bind an entity via channel API (bypasses 6-digit code).
16
16
  * If entityId is omitted, the backend auto-selects the first free slot.
17
17
  */
package/dist/client.js CHANGED
@@ -11,24 +11,17 @@ export class EClawClient {
11
11
  constructor(config) {
12
12
  this.apiBase = config.apiBase;
13
13
  this.apiKey = config.apiKey;
14
- this.entityId = config.entityId; // undefined until assigned by bindEntity
15
14
  }
16
15
  /** Register callback URL with E-Claw backend */
17
- async registerCallback(callbackUrl, callbackToken, callbackUsername, callbackPassword) {
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- const body = {
20
- channel_api_key: this.apiKey,
21
- callback_url: callbackUrl,
22
- callback_token: callbackToken,
23
- };
24
- if (callbackUsername && callbackPassword) {
25
- body.callback_username = callbackUsername;
26
- body.callback_password = callbackPassword;
27
- }
16
+ async registerCallback(callbackUrl, callbackToken) {
28
17
  const res = await fetch(`${this.apiBase}/api/channel/register`, {
29
18
  method: 'POST',
30
19
  headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify(body),
20
+ body: JSON.stringify({
21
+ channel_api_key: this.apiKey,
22
+ callback_url: callbackUrl,
23
+ callback_token: callbackToken,
24
+ }),
32
25
  });
33
26
  const data = await res.json();
34
27
  if (!data.success) {
package/dist/config.js CHANGED
@@ -28,7 +28,6 @@ export function resolveAccount(cfg, accountId) {
28
28
  apiKey: account?.apiKey ?? '',
29
29
  apiSecret: account?.apiSecret,
30
30
  apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
31
- entityId: account?.entityId, // undefined = auto-assign
32
31
  botName: account?.botName,
33
32
  webhookUrl: account?.webhookUrl,
34
33
  };
package/dist/gateway.js CHANGED
@@ -22,7 +22,6 @@ function resolveAccountFromCtx(ctx) {
22
22
  apiKey: ctx.account.apiKey,
23
23
  apiSecret: ctx.account.apiSecret,
24
24
  apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
25
- entityId: ctx.account.entityId, // undefined = auto-select
26
25
  botName: ctx.account.botName,
27
26
  webhookUrl: ctx.account.webhookUrl,
28
27
  };
@@ -77,20 +76,19 @@ export async function startAccount(ctx) {
77
76
  registerWebhookToken(callbackToken, accountId, handler);
78
77
  console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
79
78
  try {
80
- // Detect WEB_PASSWORD / SETUP_PASSWORD for Railway Basic Auth
81
- const webPassword = process.env.WEB_PASSWORD || process.env.SETUP_PASSWORD;
82
- const callbackUsername = webPassword ? 'admin' : undefined;
83
- const callbackPassword = webPassword || undefined;
84
79
  // Register callback with E-Claw backend
85
- const regData = await client.registerCallback(callbackUrl, callbackToken, callbackUsername, callbackPassword);
80
+ const regData = await client.registerCallback(callbackUrl, callbackToken);
86
81
  console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
87
- // Bind entity via channel API.
88
- // /api/channel/bind is idempotent for the same channel account:
82
+ // Debug: log entity slot status
83
+ for (const e of regData.entities) {
84
+ console.log(`[E-Claw] slot ${e.entityId}: ${e.character}${e.name ? ` "${e.name}"` : ''} bound=${e.isBound} bindingType=${e.bindingType ?? 'none'}`);
85
+ }
86
+ // Bind entity via channel API (always auto-select — server picks first free slot).
87
+ // entityId is NOT stored in config because slots are dynamic.
88
+ // /api/channel/bind without entityId is idempotent:
89
89
  // - Not bound → binds fresh, returns new botSecret
90
90
  // - Already bound via this channel account → returns existing botSecret (reconnect)
91
- // - Bound via different method → throws error (user must unbind first)
92
- // entityId is omitted here so the server auto-selects the best slot
93
- const bindData = await client.bindEntity(account.entityId, account.botName);
91
+ const bindData = await client.bindEntity(undefined, account.botName);
94
92
  const assignedEntityId = bindData.entityId;
95
93
  const entityInfo = regData.entities.find(e => e.entityId === assignedEntityId);
96
94
  const wasAlreadyBound = entityInfo?.isBound ?? false;
package/dist/index.js CHANGED
@@ -14,7 +14,6 @@ import { dispatchWebhook } from './webhook-registry.js';
14
14
  * default:
15
15
  * apiKey: "eck_..."
16
16
  * apiBase: "https://eclawbot.com"
17
- * entityId: 0
18
17
  * botName: "My Bot"
19
18
  * webhookUrl: "https://your-openclaw-domain.com"
20
19
  *
@@ -54,7 +54,6 @@ export const eclawOnboardingAdapter = {
54
54
  [accountId]: {
55
55
  apiKey: String(apiKey).trim(),
56
56
  apiBase: resolved.apiBase || 'https://eclawbot.com',
57
- entityId: resolved.entityId, // keep existing if re-configuring, else undefined = auto-assign
58
57
  botName: String(botName).trim() || undefined,
59
58
  webhookUrl: String(webhookUrl).trim() || undefined,
60
59
  enabled: true,
package/dist/types.d.ts CHANGED
@@ -4,7 +4,6 @@ export interface EClawAccountConfig {
4
4
  apiKey: string;
5
5
  apiSecret?: string;
6
6
  apiBase: string;
7
- entityId?: number;
8
7
  botName?: string;
9
8
  webhookUrl?: string;
10
9
  }
@@ -15,4 +15,5 @@
15
15
  * - Mission context via eclaw_context.missionHints
16
16
  * - Silent suppression via silentToken (default "[SILENT]")
17
17
  */
18
- export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
18
+ export declare function createWebhookHandler(_expectedToken: string, // kept for API compat; auth is handled by webhook-registry
19
+ accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
@@ -17,19 +17,15 @@ import { getClient, setActiveEvent, clearActiveEvent } from './outbound.js';
17
17
  * - Mission context via eclaw_context.missionHints
18
18
  * - Silent suppression via silentToken (default "[SILENT]")
19
19
  */
20
- export function createWebhookHandler(expectedToken, accountId,
20
+ export function createWebhookHandler(_expectedToken, // kept for API compat; auth is handled by webhook-registry
21
+ accountId,
21
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
23
  cfg // full openclaw config (ctx.cfg from startAccount)
23
24
  ) {
24
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
26
  return async (req, res) => {
26
- // Verify callback token
27
- const authHeader = req.headers?.authorization;
28
- if (expectedToken && (!authHeader || authHeader !== `Bearer ${expectedToken}`)) {
29
- res.writeHead(401, { 'Content-Type': 'application/json' });
30
- res.end(JSON.stringify({ error: 'Unauthorized' }));
31
- return;
32
- }
27
+ // Token verification is handled by webhook-registry dispatch.
28
+ // No additional auth check needed here.
33
29
  const msg = req.body;
34
30
  // ACK immediately so E-Claw doesn't time out
35
31
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -21,23 +21,31 @@ export function unregisterWebhookToken(callbackToken) {
21
21
  */
22
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
23
  export async function dispatchWebhook(req, res) {
24
- // Support two auth modes:
25
- // 1. Standard: Authorization: Bearer <token>
26
- // 2. Basic Auth gateway (Railway WEB_PASSWORD): X-Callback-Token header
27
24
  const authHeader = req.headers?.authorization;
28
- const customToken = req.headers?.['x-callback-token'];
29
- const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : customToken;
30
- if (!token) {
31
- res.writeHead(401, { 'Content-Type': 'application/json' });
32
- res.end(JSON.stringify({ error: 'Auth required' }));
33
- return;
25
+ // Try Bearer-token routing first (preferred)
26
+ if (authHeader?.startsWith('Bearer ')) {
27
+ const token = authHeader.slice(7);
28
+ const entry = registry.get(token);
29
+ if (entry) {
30
+ await entry.handler(req, res);
31
+ return;
32
+ }
33
+ // Token present but unknown — fall through to single-handler fallback
34
34
  }
35
- const entry = registry.get(token);
36
- if (!entry) {
37
- // Unknown token likely a stale push after a server restart
38
- res.writeHead(404, { 'Content-Type': 'application/json' });
39
- res.end(JSON.stringify({ error: 'Unknown token' }));
35
+ // Fallback: if exactly one handler is registered, route to it.
36
+ // This handles E-Claw backends that don't echo callback_token.
37
+ if (registry.size === 1) {
38
+ const [, entry] = [...registry.entries()][0];
39
+ await entry.handler(req, res);
40
40
  return;
41
41
  }
42
- await entry.handler(req, res);
42
+ // No valid routing possible
43
+ if (registry.size === 0) {
44
+ res.writeHead(503, { 'Content-Type': 'application/json' });
45
+ res.end(JSON.stringify({ error: 'No handlers registered' }));
46
+ }
47
+ else {
48
+ res.writeHead(401, { 'Content-Type': 'application/json' });
49
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
50
+ }
43
51
  }
@@ -1,27 +1,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
- }
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
+ "botName": { "type": "string", "maxLength": 20 }
20
+ },
21
+ "required": ["apiKey", "apiSecret"]
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@eclaw/openclaw-channel",
3
- "version": "1.1.7",
4
- "description": "E-Claw channel plugin for OpenClaw — AI chat platform for live wallpaper entities",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- }
13
- },
14
- "files": [
15
- "dist/",
16
- "openclaw.plugin.json",
17
- "README.md"
18
- ],
19
- "scripts": {
20
- "build": "tsc",
21
- "dev": "tsc --watch",
22
- "test": "vitest run",
23
- "lint": "tsc --noEmit",
24
- "prepublishOnly": "npm run build"
25
- },
26
- "openclaw": {
27
- "extensions": [
28
- "./dist/index.js"
29
- ],
30
- "channel": {
31
- "id": "eclaw",
32
- "label": "E-Claw",
33
- "selectionLabel": "E-Claw (AI Live Wallpaper Chat)",
34
- "docsPath": "https://github.com/HankHuang0516/openclaw-channel-eclaw#readme",
35
- "description": "Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android."
36
- },
37
- "install": {
38
- "npmSpec": "@eclaw/openclaw-channel"
39
- }
40
- },
41
- "keywords": [
42
- "openclaw",
43
- "openclaw-channel",
44
- "channel",
45
- "eclaw",
46
- "ai-agent",
47
- "live-wallpaper"
48
- ],
49
- "author": "HankHuang",
50
- "license": "MIT",
51
- "repository": {
52
- "type": "git",
53
- "url": "https://github.com/HankHuang0516/openclaw-channel-eclaw"
54
- },
55
- "devDependencies": {
56
- "typescript": "^5.4",
57
- "vitest": "^2.0",
58
- "@types/node": "^20"
59
- }
60
- }
1
+ {
2
+ "name": "@eclaw/openclaw-channel",
3
+ "version": "1.2.1",
4
+ "description": "E-Claw channel plugin for OpenClaw — AI chat platform for live wallpaper entities",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "openclaw.plugin.json",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "test": "vitest run",
23
+ "lint": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "openclaw": {
27
+ "extensions": [
28
+ "./dist/index.js"
29
+ ],
30
+ "channel": {
31
+ "id": "eclaw",
32
+ "label": "E-Claw",
33
+ "selectionLabel": "E-Claw (AI Live Wallpaper Chat)",
34
+ "docsPath": "https://github.com/HankHuang0516/openclaw-channel-eclaw#readme",
35
+ "description": "Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android."
36
+ },
37
+ "install": {
38
+ "npmSpec": "@eclaw/openclaw-channel"
39
+ }
40
+ },
41
+ "keywords": [
42
+ "openclaw",
43
+ "openclaw-channel",
44
+ "channel",
45
+ "eclaw",
46
+ "ai-agent",
47
+ "live-wallpaper"
48
+ ],
49
+ "author": "HankHuang",
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/HankHuang0516/openclaw-channel-eclaw"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.19.37",
57
+ "typescript": "^5.4",
58
+ "vitest": "^2.0"
59
+ }
60
+ }