@eclaw/openclaw-channel 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -58
- package/dist/channel.d.ts +19 -0
- package/dist/channel.js +4 -2
- package/dist/client.d.ts +10 -5
- package/dist/client.js +57 -14
- package/dist/config.js +16 -4
- package/dist/gateway.d.ts +5 -6
- package/dist/gateway.js +89 -165
- package/dist/index.d.ts +0 -21
- package/dist/index.js +35 -6
- package/dist/onboarding.d.ts +19 -0
- package/dist/onboarding.js +77 -0
- package/dist/outbound.d.ts +2 -0
- package/dist/outbound.js +18 -0
- package/dist/types.d.ts +19 -2
- package/dist/webhook-handler.d.ts +14 -3
- package/dist/webhook-handler.js +111 -29
- package/dist/webhook-registry.d.ts +19 -0
- package/dist/webhook-registry.js +39 -0
- package/openclaw.plugin.json +27 -27
- package/package.json +60 -60
package/README.md
CHANGED
|
@@ -1,58 +1,244 @@
|
|
|
1
|
-
# @eclaw/openclaw-channel
|
|
2
|
-
|
|
3
|
-
OpenClaw channel plugin for [E-Claw](https://eclawbot.com) —
|
|
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
|
-
```bash
|
|
10
|
-
npm install @eclaw/openclaw-channel
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Configuration
|
|
14
|
-
|
|
15
|
-
Add to your OpenClaw `config.yaml`:
|
|
16
|
-
|
|
17
|
-
```yaml
|
|
18
|
-
plugins:
|
|
19
|
-
- "@eclaw/openclaw-channel"
|
|
20
|
-
|
|
21
|
-
channels:
|
|
22
|
-
eclaw:
|
|
23
|
-
accounts:
|
|
24
|
-
default:
|
|
25
|
-
apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
|
|
26
|
-
apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
|
|
27
|
-
apiBase: "https://eclawbot.com"
|
|
28
|
-
entityId: 0 #
|
|
29
|
-
botName: "My Bot"
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Getting API Credentials
|
|
33
|
-
|
|
34
|
-
1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
|
|
35
|
-
2. Go to **Settings → Channel API**
|
|
36
|
-
3. Copy your `API Key` and `API Secret`
|
|
37
|
-
|
|
38
|
-
## How It Works
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
|
|
42
|
-
OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
- **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
|
|
46
|
-
- **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
|
|
47
|
-
- **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
|
|
48
|
-
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
```bash
|
|
10
|
+
npm install @eclaw/openclaw-channel
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Add to your OpenClaw `config.yaml`:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
plugins:
|
|
19
|
+
- "@eclaw/openclaw-channel"
|
|
20
|
+
|
|
21
|
+
channels:
|
|
22
|
+
eclaw:
|
|
23
|
+
accounts:
|
|
24
|
+
default:
|
|
25
|
+
apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
|
|
26
|
+
apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
|
|
27
|
+
apiBase: "https://eclawbot.com"
|
|
28
|
+
entityId: 0 # Entity slot (0-3 free tier, 0-7 premium). Omit to auto-assign.
|
|
29
|
+
botName: "My Bot" # Display name in E-Claw (max 20 chars)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Getting API Credentials
|
|
33
|
+
|
|
34
|
+
1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
|
|
35
|
+
2. Go to **Settings → Channel API**
|
|
36
|
+
3. Copy your `API Key` (`eck_...`) and `API Secret` (`ecs_...`)
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
User (Android) ──speaks──▶ E-Claw Backend ──webhook──▶ OpenClaw Agent
|
|
42
|
+
OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (Android)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **Inbound**: E-Claw POSTs structured JSON to a webhook URL registered by this plugin
|
|
46
|
+
- **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
|
|
47
|
+
- **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
|
|
48
|
+
|
|
49
|
+
## Inbound Message Structure
|
|
50
|
+
|
|
51
|
+
Every message delivered to your webhook has this shape:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"event": "message",
|
|
56
|
+
"from": "user",
|
|
57
|
+
"deviceId": "...",
|
|
58
|
+
"entityId": 0,
|
|
59
|
+
"conversationId": "...:0",
|
|
60
|
+
"text": "Hello!",
|
|
61
|
+
"timestamp": 1741234567890,
|
|
62
|
+
"isBroadcast": false,
|
|
63
|
+
"eclaw_context": {
|
|
64
|
+
"expectsReply": true,
|
|
65
|
+
"silentToken": "[SILENT]",
|
|
66
|
+
"missionHints": "..."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `event` values
|
|
72
|
+
|
|
73
|
+
| Value | Description |
|
|
74
|
+
|-------|-------------|
|
|
75
|
+
| `message` | Normal message from the device user |
|
|
76
|
+
| `entity_message` | Bot-to-bot message (another entity spoke directly to yours) |
|
|
77
|
+
| `broadcast` | Broadcast from another entity (one-to-many) |
|
|
78
|
+
|
|
79
|
+
### `from` values
|
|
80
|
+
|
|
81
|
+
| Value | Description |
|
|
82
|
+
|-------|-------------|
|
|
83
|
+
| `user` | Human user on the Android device |
|
|
84
|
+
| `system` | Server-generated event (name change, entity moved, etc.) |
|
|
85
|
+
| `scheduled` | Scheduled message created by the device owner |
|
|
86
|
+
|
|
87
|
+
## `eclaw_context` — Channel Bot Parity
|
|
88
|
+
|
|
89
|
+
Since v1.0.17, every inbound push includes an `eclaw_context` block that gives your bot the same awareness as traditional push-based bots:
|
|
90
|
+
|
|
91
|
+
| Field | Type | Description |
|
|
92
|
+
|-------|------|-------------|
|
|
93
|
+
| `expectsReply` | `boolean` | `false` for system events and quota-exceeded bot messages — your bot should output `silentToken` to stay quiet |
|
|
94
|
+
| `silentToken` | `string` | Output this exact string to suppress all API calls (default: `"[SILENT]"`) |
|
|
95
|
+
| `missionHints` | `string` | API reference for reading/writing mission tasks (TODO, SKILL, RULE, SOUL) for this entity |
|
|
96
|
+
| `b2bRemaining` | `number` | Remaining bot-to-bot reply quota for this conversation (resets on human message) |
|
|
97
|
+
| `b2bMax` | `number` | Maximum bot-to-bot quota (currently 8) |
|
|
98
|
+
|
|
99
|
+
### Staying Silent
|
|
100
|
+
|
|
101
|
+
When `expectsReply` is `false`, output the `silentToken` to avoid sending an unwanted reply:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
User message: [SYSTEM:ENTITY_MOVED] Your entity slot has changed...
|
|
105
|
+
Bot reply: [SILENT] ← plugin suppresses all API calls
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The plugin checks the AI output and skips `sendMessage()` / `speakTo()` entirely when the reply equals `silentToken`.
|
|
109
|
+
|
|
110
|
+
## System Events
|
|
111
|
+
|
|
112
|
+
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`.
|
|
113
|
+
|
|
114
|
+
| Event tag in text | Trigger |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `[SYSTEM:ENTITY_MOVED]` | Device owner reordered entities — your bot's slot changed |
|
|
117
|
+
| `[SYSTEM:NAME_CHANGED]` | Device owner renamed this entity |
|
|
118
|
+
|
|
119
|
+
Example `ENTITY_MOVED` payload text:
|
|
120
|
+
```
|
|
121
|
+
[SYSTEM:ENTITY_MOVED] Your entity slot has changed from #1 to #2.
|
|
122
|
+
|
|
123
|
+
UPDATED CREDENTIALS:
|
|
124
|
+
- entityId: 2 (was 1)
|
|
125
|
+
- deviceId: ...
|
|
126
|
+
- botSecret: ...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Bot-to-Bot Messages (`entity_message` / `broadcast`)
|
|
130
|
+
|
|
131
|
+
When another E-Claw entity sends your bot a message, the plugin automatically enriches the body before dispatching to your OpenClaw agent:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
[Bot-to-Bot message from Entity 2 (LOBSTER)]
|
|
135
|
+
[Quota: 7/8 remaining — output "[SILENT]" if no new info worth replying to]
|
|
136
|
+
<mission API hints>
|
|
137
|
+
Hello! How are you?
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
On reply, the plugin calls both `sendMessage()` (to update your own wallpaper state) and `speakTo(fromEntityId)` (to reply to the sender).
|
|
141
|
+
|
|
142
|
+
## Scheduled Messages
|
|
143
|
+
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
## Environment Variables
|
|
147
|
+
|
|
148
|
+
| Variable | Required | Description |
|
|
149
|
+
|----------|----------|-------------|
|
|
150
|
+
| `ECLAW_WEBHOOK_URL` | Production | Public URL for receiving inbound messages |
|
|
151
|
+
| `ECLAW_WEBHOOK_PORT` | Optional | Webhook server port (default: random) |
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
### `Config invalid: channels.eclaw unknown channel id`
|
|
156
|
+
|
|
157
|
+
**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.
|
|
158
|
+
|
|
159
|
+
**Fix**: Run this script in the Zeabur terminal, then do a **full container restart** from the Zeabur Dashboard (not SIGUSR1 in-process restart):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
cat > /tmp/fix-cfg.js << 'EOF'
|
|
163
|
+
var fs = require('fs');
|
|
164
|
+
var p = '/home/node/.openclaw/openclaw.json';
|
|
165
|
+
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
166
|
+
if (cfg.plugins && cfg.plugins.installs) {
|
|
167
|
+
delete cfg.plugins.installs['openclaw-channel'];
|
|
168
|
+
}
|
|
169
|
+
if (cfg.plugins && cfg.plugins.entries) {
|
|
170
|
+
delete cfg.plugins.entries['openclaw-channel'];
|
|
171
|
+
}
|
|
172
|
+
cfg.plugins = cfg.plugins || {};
|
|
173
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
174
|
+
if (!cfg.plugins.allow.includes('openclaw-channel')) {
|
|
175
|
+
cfg.plugins.allow.push('openclaw-channel');
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
178
|
+
console.log('Done:', JSON.stringify(cfg.plugins, null, 2));
|
|
179
|
+
EOF
|
|
180
|
+
node /tmp/fix-cfg.js
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `plugin already exists: delete it first` (on upgrade)
|
|
186
|
+
|
|
187
|
+
Running `openclaw plugins install @eclaw/openclaw-channel@X.Y.Z` directly fails when an older version is present. Use this full upgrade script instead:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cat > /tmp/upgrade-eclaw.js << 'EOF'
|
|
191
|
+
var fs = require('fs'), { execSync } = require('child_process');
|
|
192
|
+
var p = '/home/node/.openclaw/openclaw.json';
|
|
193
|
+
var cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
194
|
+
|
|
195
|
+
// 1. Save eclaw channel config
|
|
196
|
+
var saved = cfg.channels && cfg.channels.eclaw;
|
|
197
|
+
|
|
198
|
+
// 2. Strip entries that cause validation to fail
|
|
199
|
+
if (cfg.channels) delete cfg.channels.eclaw;
|
|
200
|
+
if (cfg.plugins) {
|
|
201
|
+
if (cfg.plugins.entries) delete cfg.plugins.entries['openclaw-channel'];
|
|
202
|
+
if (cfg.plugins.allow) cfg.plugins.allow = cfg.plugins.allow.filter(x => x !== 'openclaw-channel');
|
|
203
|
+
if (cfg.plugins.installs) delete cfg.plugins.installs['openclaw-channel'];
|
|
204
|
+
}
|
|
205
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
206
|
+
|
|
207
|
+
// 3. Remove old plugin files
|
|
208
|
+
execSync('rm -rf /home/node/.openclaw/extensions/openclaw-channel');
|
|
209
|
+
|
|
210
|
+
// 4. Install new version (update version number below)
|
|
211
|
+
var out = execSync('openclaw plugins install @eclaw/openclaw-channel@1.0.18 2>&1', { encoding: 'utf8' });
|
|
212
|
+
console.log(out);
|
|
213
|
+
|
|
214
|
+
// 5. Restore channel config
|
|
215
|
+
cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
216
|
+
if (saved) { cfg.channels = cfg.channels || {}; cfg.channels.eclaw = saved; }
|
|
217
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
218
|
+
if (!cfg.plugins.allow.includes('openclaw-channel')) cfg.plugins.allow.push('openclaw-channel');
|
|
219
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
220
|
+
console.log('Done — restart the service from Zeabur Dashboard.');
|
|
221
|
+
EOF
|
|
222
|
+
node /tmp/upgrade-eclaw.js
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
After the script completes, do a **full service restart** from Zeabur Dashboard.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### In-process restart (`SIGUSR1`) doesn't apply channel config changes
|
|
230
|
+
|
|
231
|
+
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.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### Bot doesn't receive messages / webhook not called
|
|
236
|
+
|
|
237
|
+
1. Check `ECLAW_WEBHOOK_URL` is a publicly reachable URL (not `localhost`)
|
|
238
|
+
2. Verify the callback was registered: the plugin logs `Account default ready!` on startup
|
|
239
|
+
3. In E-Claw Portal, confirm the entity shows as channel-bound (green dot)
|
|
240
|
+
4. Check server logs: `curl "https://eclawbot.com/api/logs?deviceId=...&deviceSecret=...&limit=20"`
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
package/dist/channel.d.ts
CHANGED
|
@@ -40,4 +40,23 @@ export declare const eclawChannel: {
|
|
|
40
40
|
gateway: {
|
|
41
41
|
startAccount: typeof startAccount;
|
|
42
42
|
};
|
|
43
|
+
onboarding: {
|
|
44
|
+
channel: string;
|
|
45
|
+
getStatus: ({ cfg }: {
|
|
46
|
+
cfg: any;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
channel: string;
|
|
49
|
+
configured: boolean;
|
|
50
|
+
statusLines: string[];
|
|
51
|
+
selectionHint: string;
|
|
52
|
+
quickstartScore: number;
|
|
53
|
+
}>;
|
|
54
|
+
configure: ({ cfg, prompter }: {
|
|
55
|
+
cfg: any;
|
|
56
|
+
prompter: any;
|
|
57
|
+
}) => Promise<{
|
|
58
|
+
cfg: any;
|
|
59
|
+
accountId: string;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
43
62
|
};
|
package/dist/channel.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listAccountIds, resolveAccount } from './config.js';
|
|
2
2
|
import { sendText, sendMedia } from './outbound.js';
|
|
3
3
|
import { startAccount } from './gateway.js';
|
|
4
|
+
import { eclawOnboardingAdapter } from './onboarding.js';
|
|
4
5
|
/**
|
|
5
6
|
* E-Claw ChannelPlugin definition.
|
|
6
7
|
*
|
|
@@ -13,9 +14,9 @@ export const eclawChannel = {
|
|
|
13
14
|
meta: {
|
|
14
15
|
id: 'eclaw',
|
|
15
16
|
label: 'E-Claw',
|
|
16
|
-
selectionLabel: 'E-Claw (AI
|
|
17
|
+
selectionLabel: 'E-Claw (AI Live Wallpaper Chat)',
|
|
17
18
|
docsPath: '/channels/eclaw',
|
|
18
|
-
blurb: 'Connect OpenClaw to E-Claw —
|
|
19
|
+
blurb: 'Connect OpenClaw to E-Claw — an AI chat platform for live wallpaper entities on Android.',
|
|
19
20
|
aliases: ['eclaw', 'claw', 'e-claw'],
|
|
20
21
|
},
|
|
21
22
|
capabilities: {
|
|
@@ -40,4 +41,5 @@ export const eclawChannel = {
|
|
|
40
41
|
gateway: {
|
|
41
42
|
startAccount,
|
|
42
43
|
},
|
|
44
|
+
onboarding: eclawOnboardingAdapter,
|
|
43
45
|
};
|
package/dist/client.d.ts
CHANGED
|
@@ -6,20 +6,25 @@ import type { EClawAccountConfig, RegisterResponse, BindResponse, MessageRespons
|
|
|
6
6
|
export declare class EClawClient {
|
|
7
7
|
private readonly apiBase;
|
|
8
8
|
private readonly apiKey;
|
|
9
|
-
private readonly apiSecret;
|
|
10
9
|
private deviceId;
|
|
11
10
|
private botSecret;
|
|
12
11
|
private entityId;
|
|
13
12
|
constructor(config: EClawAccountConfig);
|
|
14
13
|
/** Register callback URL with E-Claw backend */
|
|
15
14
|
registerCallback(callbackUrl: string, callbackToken: string): Promise<RegisterResponse>;
|
|
16
|
-
/** Bind an entity via channel API (bypasses 6-digit code)
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/** Bind an entity via channel API (bypasses 6-digit code).
|
|
16
|
+
* If entityId is omitted, the backend auto-selects the first free slot.
|
|
17
|
+
*/
|
|
18
|
+
bindEntity(entityId?: number, name?: string): Promise<BindResponse>;
|
|
19
|
+
/** Send bot message to user (updates own entity state on wallpaper) */
|
|
19
20
|
sendMessage(message: string, state?: string, mediaType?: string, mediaUrl?: string): Promise<MessageResponse>;
|
|
21
|
+
/** Send bot-to-bot message to another entity (speak-to) */
|
|
22
|
+
speakTo(toEntityId: number, text: string, expectsReply?: boolean): Promise<void>;
|
|
23
|
+
/** Broadcast message to all other bound entities */
|
|
24
|
+
broadcastToAll(text: string, expectsReply?: boolean): Promise<void>;
|
|
20
25
|
/** Unregister callback on shutdown */
|
|
21
26
|
unregisterCallback(): Promise<void>;
|
|
22
27
|
get currentDeviceId(): string | null;
|
|
23
28
|
get currentBotSecret(): string | null;
|
|
24
|
-
get currentEntityId(): number;
|
|
29
|
+
get currentEntityId(): number | undefined;
|
|
25
30
|
}
|
package/dist/client.js
CHANGED
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
export class EClawClient {
|
|
6
6
|
apiBase;
|
|
7
7
|
apiKey;
|
|
8
|
-
apiSecret;
|
|
9
8
|
deviceId = null;
|
|
10
9
|
botSecret = null;
|
|
11
10
|
entityId;
|
|
12
11
|
constructor(config) {
|
|
13
12
|
this.apiBase = config.apiBase;
|
|
14
13
|
this.apiKey = config.apiKey;
|
|
15
|
-
this.
|
|
16
|
-
this.entityId = config.entityId;
|
|
14
|
+
this.entityId = config.entityId; // undefined until assigned by bindEntity
|
|
17
15
|
}
|
|
18
16
|
/** Register callback URL with E-Claw backend */
|
|
19
17
|
async registerCallback(callbackUrl, callbackToken) {
|
|
@@ -22,7 +20,6 @@ export class EClawClient {
|
|
|
22
20
|
headers: { 'Content-Type': 'application/json' },
|
|
23
21
|
body: JSON.stringify({
|
|
24
22
|
channel_api_key: this.apiKey,
|
|
25
|
-
channel_api_secret: this.apiSecret,
|
|
26
23
|
callback_url: callbackUrl,
|
|
27
24
|
callback_token: callbackToken,
|
|
28
25
|
}),
|
|
@@ -34,28 +31,40 @@ export class EClawClient {
|
|
|
34
31
|
this.deviceId = data.deviceId;
|
|
35
32
|
return data;
|
|
36
33
|
}
|
|
37
|
-
/** Bind an entity via channel API (bypasses 6-digit code)
|
|
34
|
+
/** Bind an entity via channel API (bypasses 6-digit code).
|
|
35
|
+
* If entityId is omitted, the backend auto-selects the first free slot.
|
|
36
|
+
*/
|
|
38
37
|
async bindEntity(entityId, name) {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const body = { channel_api_key: this.apiKey };
|
|
40
|
+
if (entityId !== undefined)
|
|
41
|
+
body.entityId = entityId;
|
|
42
|
+
if (name)
|
|
43
|
+
body.name = name;
|
|
39
44
|
const res = await fetch(`${this.apiBase}/api/channel/bind`, {
|
|
40
45
|
method: 'POST',
|
|
41
46
|
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify(
|
|
43
|
-
channel_api_key: this.apiKey,
|
|
44
|
-
channel_api_secret: this.apiSecret,
|
|
45
|
-
entityId,
|
|
46
|
-
name: name || undefined,
|
|
47
|
-
}),
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
48
|
});
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
50
|
const data = await res.json();
|
|
50
51
|
if (!data.success) {
|
|
52
|
+
// Build a detailed error message when all slots are full
|
|
53
|
+
if (res.status === 409 && data.entities) {
|
|
54
|
+
const list = data.entities
|
|
55
|
+
.map((e) => ` slot ${e.entityId} (${e.character})${e.name ? ` "${e.name}"` : ''}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
throw new Error(`${data.message}\nCurrent entities:\n${list}\n` +
|
|
58
|
+
'Add entityId to your channel config to target a specific slot after unbinding it.');
|
|
59
|
+
}
|
|
51
60
|
throw new Error(data.message || `Bind failed (HTTP ${res.status})`);
|
|
52
61
|
}
|
|
53
62
|
this.botSecret = data.botSecret;
|
|
54
63
|
this.deviceId = data.deviceId;
|
|
55
|
-
this.entityId = entityId;
|
|
64
|
+
this.entityId = data.entityId; // Use server-assigned slot
|
|
56
65
|
return data;
|
|
57
66
|
}
|
|
58
|
-
/** Send bot message to user */
|
|
67
|
+
/** Send bot message to user (updates own entity state on wallpaper) */
|
|
59
68
|
async sendMessage(message, state = 'IDLE', mediaType, mediaUrl) {
|
|
60
69
|
if (!this.deviceId || !this.botSecret) {
|
|
61
70
|
throw new Error('Not bound — call bindEntity() first');
|
|
@@ -76,6 +85,41 @@ export class EClawClient {
|
|
|
76
85
|
});
|
|
77
86
|
return await res.json();
|
|
78
87
|
}
|
|
88
|
+
/** Send bot-to-bot message to another entity (speak-to) */
|
|
89
|
+
async speakTo(toEntityId, text, expectsReply = false) {
|
|
90
|
+
if (!this.deviceId || !this.botSecret) {
|
|
91
|
+
throw new Error('Not bound — call bindEntity() first');
|
|
92
|
+
}
|
|
93
|
+
await fetch(`${this.apiBase}/api/entity/speak-to`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
deviceId: this.deviceId,
|
|
98
|
+
fromEntityId: this.entityId,
|
|
99
|
+
toEntityId,
|
|
100
|
+
botSecret: this.botSecret,
|
|
101
|
+
text,
|
|
102
|
+
expects_reply: expectsReply,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Broadcast message to all other bound entities */
|
|
107
|
+
async broadcastToAll(text, expectsReply = false) {
|
|
108
|
+
if (!this.deviceId || !this.botSecret) {
|
|
109
|
+
throw new Error('Not bound — call bindEntity() first');
|
|
110
|
+
}
|
|
111
|
+
await fetch(`${this.apiBase}/api/entity/broadcast`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
deviceId: this.deviceId,
|
|
116
|
+
fromEntityId: this.entityId,
|
|
117
|
+
botSecret: this.botSecret,
|
|
118
|
+
text,
|
|
119
|
+
expects_reply: expectsReply,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
79
123
|
/** Unregister callback on shutdown */
|
|
80
124
|
async unregisterCallback() {
|
|
81
125
|
await fetch(`${this.apiBase}/api/channel/register`, {
|
|
@@ -83,7 +127,6 @@ export class EClawClient {
|
|
|
83
127
|
headers: { 'Content-Type': 'application/json' },
|
|
84
128
|
body: JSON.stringify({
|
|
85
129
|
channel_api_key: this.apiKey,
|
|
86
|
-
channel_api_secret: this.apiSecret,
|
|
87
130
|
}),
|
|
88
131
|
});
|
|
89
132
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
/** Extract accounts map from full openclaw config or eclaw-specific config */
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
function getAccounts(cfg) {
|
|
4
|
+
// Full openclaw config: { channels: { eclaw: { accounts: {...} } } }
|
|
5
|
+
if (cfg?.channels?.eclaw?.accounts)
|
|
6
|
+
return cfg.channels.eclaw.accounts;
|
|
7
|
+
// Eclaw channel config: { accounts: {...} }
|
|
8
|
+
if (cfg?.accounts && typeof cfg.accounts === 'object')
|
|
9
|
+
return cfg.accounts;
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
1
12
|
/** List all configured account IDs from OpenClaw config */
|
|
2
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
14
|
export function listAccountIds(cfg) {
|
|
4
|
-
const accounts = cfg
|
|
15
|
+
const accounts = getAccounts(cfg);
|
|
5
16
|
if (!accounts || typeof accounts !== 'object')
|
|
6
17
|
return [];
|
|
7
18
|
return Object.keys(accounts);
|
|
@@ -9,15 +20,16 @@ export function listAccountIds(cfg) {
|
|
|
9
20
|
/** Resolve a specific account's config, with defaults */
|
|
10
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
22
|
export function resolveAccount(cfg, accountId) {
|
|
12
|
-
const accounts = cfg
|
|
23
|
+
const accounts = getAccounts(cfg);
|
|
13
24
|
const id = accountId ?? Object.keys(accounts)[0] ?? 'default';
|
|
14
25
|
const account = accounts[id];
|
|
15
26
|
return {
|
|
16
27
|
enabled: account?.enabled ?? true,
|
|
17
28
|
apiKey: account?.apiKey ?? '',
|
|
18
|
-
apiSecret: account?.apiSecret
|
|
29
|
+
apiSecret: account?.apiSecret,
|
|
19
30
|
apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
|
|
20
|
-
entityId: account?.entityId
|
|
31
|
+
entityId: account?.entityId, // undefined = auto-assign
|
|
21
32
|
botName: account?.botName,
|
|
33
|
+
webhookUrl: account?.webhookUrl,
|
|
22
34
|
};
|
|
23
35
|
}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gateway lifecycle: start an E-Claw account.
|
|
3
3
|
*
|
|
4
|
-
* 1.
|
|
5
|
-
* 2.
|
|
6
|
-
*
|
|
4
|
+
* 1. Resolve credentials from ctx.account or disk
|
|
5
|
+
* 2. Register a per-session handler in the webhook-registry (served by the
|
|
6
|
+
* main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
|
|
7
|
+
* 3. Register callback URL with E-Claw backend
|
|
7
8
|
* 4. Auto-bind entity if not already bound
|
|
8
|
-
* 5.
|
|
9
|
-
* 6. On health-check failure, reconnect with exponential backoff
|
|
10
|
-
* 7. Keep the promise alive until abort signal fires
|
|
9
|
+
* 5. Keep the promise alive until abort signal fires
|
|
11
10
|
*/
|
|
12
11
|
export declare function startAccount(ctx: any): Promise<void>;
|