@eclaw/openclaw-channel 1.0.16 → 1.0.18
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 +100 -3
- package/dist/outbound.d.ts +2 -0
- package/dist/outbound.js +18 -0
- package/dist/types.d.ts +9 -0
- package/dist/webhook-handler.d.ts +8 -2
- package/dist/webhook-handler.js +60 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,15 +25,15 @@ channels:
|
|
|
25
25
|
apiKey: "eck_..." # From E-Claw Portal → Settings → Channel API
|
|
26
26
|
apiSecret: "ecs_..." # From E-Claw Portal → Settings → Channel API
|
|
27
27
|
apiBase: "https://eclawbot.com"
|
|
28
|
-
entityId: 0 #
|
|
29
|
-
botName: "My Bot"
|
|
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
30
|
```
|
|
31
31
|
|
|
32
32
|
## Getting API Credentials
|
|
33
33
|
|
|
34
34
|
1. Log in to [E-Claw Portal](https://eclawbot.com/portal)
|
|
35
35
|
2. Go to **Settings → Channel API**
|
|
36
|
-
3. Copy your `API Key` and `API Secret`
|
|
36
|
+
3. Copy your `API Key` (`eck_...`) and `API Secret` (`ecs_...`)
|
|
37
37
|
|
|
38
38
|
## How It Works
|
|
39
39
|
|
|
@@ -46,6 +46,103 @@ OpenClaw Agent ──replies──▶ POST /api/channel/message ──▶ User (
|
|
|
46
46
|
- **Outbound**: Plugin calls `POST /api/channel/message` with the bot reply
|
|
47
47
|
- **Auth**: `eck_`/`ecs_` channel credentials for API auth, per-entity `botSecret` for message auth
|
|
48
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
|
+
|
|
49
146
|
## Environment Variables
|
|
50
147
|
|
|
51
148
|
| Variable | Required | Description |
|
package/dist/outbound.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EClawClient } from './client.js';
|
|
2
|
+
export declare function setActiveEvent(accountId: string, event: string): void;
|
|
3
|
+
export declare function clearActiveEvent(accountId: string): void;
|
|
2
4
|
export declare function setClient(accountId: string, client: EClawClient): void;
|
|
3
5
|
export declare function getClient(accountId: string): EClawClient | undefined;
|
|
4
6
|
/** OpenClaw outbound: send text message to E-Claw user */
|
package/dist/outbound.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Client instances keyed by accountId */
|
|
2
2
|
const clients = new Map();
|
|
3
|
+
/** Track current inbound event type per account to suppress duplicate sendMessage calls */
|
|
4
|
+
const activeEvent = new Map();
|
|
5
|
+
export function setActiveEvent(accountId, event) {
|
|
6
|
+
activeEvent.set(accountId, event);
|
|
7
|
+
}
|
|
8
|
+
export function clearActiveEvent(accountId) {
|
|
9
|
+
activeEvent.delete(accountId);
|
|
10
|
+
}
|
|
3
11
|
export function setClient(accountId, client) {
|
|
4
12
|
clients.set(accountId, client);
|
|
5
13
|
}
|
|
@@ -10,6 +18,11 @@ export function getClient(accountId) {
|
|
|
10
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
19
|
export async function sendText(ctx) {
|
|
12
20
|
const accountId = ctx.accountId ?? 'default';
|
|
21
|
+
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
22
|
+
const event = activeEvent.get(accountId) ?? 'message';
|
|
23
|
+
if (event === 'entity_message' || event === 'broadcast') {
|
|
24
|
+
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
25
|
+
}
|
|
13
26
|
const client = clients.get(accountId);
|
|
14
27
|
if (!client) {
|
|
15
28
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
@@ -32,6 +45,11 @@ export async function sendText(ctx) {
|
|
|
32
45
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
46
|
export async function sendMedia(ctx) {
|
|
34
47
|
const accountId = ctx.accountId ?? 'default';
|
|
48
|
+
// Suppress duplicate delivery for bot-to-bot events — webhook-handler's deliver handles these
|
|
49
|
+
const event = activeEvent.get(accountId) ?? 'message';
|
|
50
|
+
if (event === 'entity_message' || event === 'broadcast') {
|
|
51
|
+
return { channel: 'eclaw', messageId: '', chatId: '' };
|
|
52
|
+
}
|
|
35
53
|
const client = clients.get(accountId);
|
|
36
54
|
if (!client) {
|
|
37
55
|
return { channel: 'eclaw', messageId: '', chatId: '' };
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface EClawAccountConfig {
|
|
|
8
8
|
botName?: string;
|
|
9
9
|
webhookUrl?: string;
|
|
10
10
|
}
|
|
11
|
+
/** Context block injected by E-Claw server for Channel Bot parity with Traditional Bot */
|
|
12
|
+
export interface EClawContext {
|
|
13
|
+
b2bRemaining?: number;
|
|
14
|
+
b2bMax?: number;
|
|
15
|
+
expectsReply?: boolean;
|
|
16
|
+
missionHints?: string;
|
|
17
|
+
silentToken?: string;
|
|
18
|
+
}
|
|
11
19
|
/** Inbound message from E-Claw callback webhook */
|
|
12
20
|
export interface EClawInboundMessage {
|
|
13
21
|
event: 'message' | 'entity_message' | 'broadcast' | 'cross_device_message';
|
|
@@ -25,6 +33,7 @@ export interface EClawInboundMessage {
|
|
|
25
33
|
fromEntityId?: number;
|
|
26
34
|
fromCharacter?: string;
|
|
27
35
|
fromPublicCode?: string;
|
|
36
|
+
eclaw_context?: EClawContext;
|
|
28
37
|
}
|
|
29
38
|
/** Entity info returned by channel register */
|
|
30
39
|
export interface EClawEntityInfo {
|
|
@@ -3,10 +3,16 @@
|
|
|
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 speakTo(fromEntityId)
|
|
7
|
-
* - 'broadcast' → Broadcast from another entity; reply via speakTo(fromEntityId)
|
|
6
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage() + speakTo(fromEntityId)
|
|
7
|
+
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
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
|
+
*
|
|
12
|
+
* Channel Bot Context Parity v1.0.17:
|
|
13
|
+
* - Bot-to-bot / broadcast now calls sendMessage() to update own wallpaper AND speakTo() to reply
|
|
14
|
+
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
15
|
+
* - Mission context via eclaw_context.missionHints
|
|
16
|
+
* - Silent suppression via silentToken (default "[SILENT]")
|
|
11
17
|
*/
|
|
12
18
|
export declare function createWebhookHandler(expectedToken: string, accountId: string, cfg: any): (req: any, res: any) => Promise<void>;
|
package/dist/webhook-handler.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { getPluginRuntime } from './runtime.js';
|
|
2
|
-
import { getClient } from './outbound.js';
|
|
2
|
+
import { getClient, setActiveEvent, clearActiveEvent } from './outbound.js';
|
|
3
3
|
/**
|
|
4
4
|
* Create an HTTP request handler for inbound messages from E-Claw.
|
|
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 speakTo(fromEntityId)
|
|
9
|
-
* - 'broadcast' → Broadcast from another entity; reply via speakTo(fromEntityId)
|
|
8
|
+
* - 'entity_message' → Bot-to-bot speak-to; reply via sendMessage() + speakTo(fromEntityId)
|
|
9
|
+
* - 'broadcast' → Broadcast from another entity; reply via sendMessage() + speakTo(fromEntityId)
|
|
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
|
+
*
|
|
14
|
+
* Channel Bot Context Parity v1.0.17:
|
|
15
|
+
* - Bot-to-bot / broadcast now calls sendMessage() to update own wallpaper AND speakTo() to reply
|
|
16
|
+
* - Quota awareness via eclaw_context.b2bRemaining / b2bMax
|
|
17
|
+
* - Mission context via eclaw_context.missionHints
|
|
18
|
+
* - Silent suppression via silentToken (default "[SILENT]")
|
|
13
19
|
*/
|
|
14
20
|
export function createWebhookHandler(expectedToken, accountId,
|
|
15
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -37,6 +43,9 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
37
43
|
const event = msg.event || 'message';
|
|
38
44
|
const fromEntityId = msg.fromEntityId;
|
|
39
45
|
const fromCharacter = msg.fromCharacter;
|
|
46
|
+
// Read server-injected context block (Channel Bot parity)
|
|
47
|
+
const eclawCtx = msg.eclaw_context;
|
|
48
|
+
const silentToken = eclawCtx?.silentToken ?? '[SILENT]';
|
|
40
49
|
// Map E-Claw media type to OpenClaw media type
|
|
41
50
|
const ocMediaType = msg.mediaType === 'photo' ? 'image'
|
|
42
51
|
: msg.mediaType === 'voice' ? 'audio'
|
|
@@ -49,10 +58,16 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
49
58
|
const senderLabel = fromCharacter
|
|
50
59
|
? `Entity ${fromEntityId} (${fromCharacter})`
|
|
51
60
|
: `Entity ${fromEntityId}`;
|
|
52
|
-
const
|
|
61
|
+
const eventPrefix = event === 'broadcast'
|
|
53
62
|
? `[Broadcast from ${senderLabel}]`
|
|
54
63
|
: `[Bot-to-Bot message from ${senderLabel}]`;
|
|
55
|
-
|
|
64
|
+
const quotaLine = eclawCtx?.b2bRemaining !== undefined
|
|
65
|
+
? `[Quota: ${eclawCtx.b2bRemaining}/${eclawCtx.b2bMax ?? 8} remaining — output "${silentToken}" if no new info worth replying to]`
|
|
66
|
+
: '';
|
|
67
|
+
const missionBlock = eclawCtx?.missionHints ?? '';
|
|
68
|
+
body = [eventPrefix, quotaLine, missionBlock, msg.text || '']
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join('\n');
|
|
56
71
|
}
|
|
57
72
|
// Build context in OpenClaw's native PascalCase format
|
|
58
73
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -75,42 +90,50 @@ cfg // full openclaw config (ctx.cfg from startAccount)
|
|
|
75
90
|
} : {}),
|
|
76
91
|
};
|
|
77
92
|
const ctxPayload = rt.channel.reply.finalizeInboundContext(inboundCtx);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Normal human message: reply via channel message
|
|
96
|
-
if (text) {
|
|
93
|
+
// Track event type so outbound.sendText() can suppress duplicate delivery
|
|
94
|
+
setActiveEvent(accountId, event);
|
|
95
|
+
try {
|
|
96
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
97
|
+
ctx: ctxPayload,
|
|
98
|
+
cfg,
|
|
99
|
+
dispatcherOptions: {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
deliver: async (payload) => {
|
|
102
|
+
if (!client)
|
|
103
|
+
return;
|
|
104
|
+
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
105
|
+
// [SILENT] token or empty → skip all API calls
|
|
106
|
+
if (!text || text === silentToken)
|
|
107
|
+
return;
|
|
108
|
+
if ((event === 'entity_message' || event === 'broadcast') && fromEntityId !== undefined) {
|
|
109
|
+
// Bot-to-bot / broadcast: update own wallpaper AND reply to sender
|
|
97
110
|
await client.sendMessage(text, 'IDLE');
|
|
111
|
+
await client.speakTo(fromEntityId, text, false);
|
|
98
112
|
}
|
|
99
|
-
else
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
else {
|
|
114
|
+
// Normal human message: reply via channel message
|
|
115
|
+
if (text) {
|
|
116
|
+
await client.sendMessage(text, 'IDLE');
|
|
117
|
+
}
|
|
118
|
+
else if (payload.mediaUrl) {
|
|
119
|
+
const rawType = typeof payload.mediaType === 'string' ? payload.mediaType : '';
|
|
120
|
+
const mediaType = rawType === 'image' ? 'photo'
|
|
121
|
+
: rawType === 'audio' ? 'voice'
|
|
122
|
+
: rawType === 'video' ? 'video'
|
|
123
|
+
: 'file';
|
|
124
|
+
await client.sendMessage('', 'IDLE', mediaType, payload.mediaUrl);
|
|
125
|
+
}
|
|
106
126
|
}
|
|
107
|
-
}
|
|
127
|
+
},
|
|
128
|
+
onError: (err) => {
|
|
129
|
+
console.error('[E-Claw] Reply delivery error:', err);
|
|
130
|
+
},
|
|
108
131
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearActiveEvent(accountId);
|
|
136
|
+
}
|
|
114
137
|
}
|
|
115
138
|
catch (err) {
|
|
116
139
|
console.error('[E-Claw] Webhook dispatch error:', err);
|